"""Health monitor -- continuous backend health checking with history tracking.

Runs a background asyncio loop that periodically pings each registered backend,
tracks uptime, latency averages, and consecutive failure counts.
"""

from __future__ import annotations

import asyncio
import contextlib
import logging
import time
from collections import deque
from datetime import datetime, timezone
from typing import Any, cast

import httpx
from pydantic import BaseModel, ConfigDict, Field

logger = logging.getLogger(__name__)

# Maximum number of recent health checks to retain per backend (for uptime calculation).
_HISTORY_SIZE = 360  # ~1 hour at 10s interval


# ---------------------------------------------------------------------------
# Data models
# ---------------------------------------------------------------------------


class HealthCheck(BaseModel):
    """Result of a single health check probe."""

    model_config = ConfigDict(frozen=False)

    backend_type: str
    healthy: bool
    latency_ms: float
    checked_at: datetime
    error: str | None = None
    details: dict[str, Any] = Field(default_factory=dict)


class BackendHealth(BaseModel):
    """Aggregated health status for a backend."""

    model_config = ConfigDict(frozen=False)

    backend_type: str
    url: str
    is_healthy: bool = False
    last_check: datetime = Field(default_factory=lambda: datetime.now(tz=timezone.utc))
    last_healthy: datetime | None = None
    consecutive_failures: int = 0
    avg_latency_ms: float = 0.0
    uptime_percent: float = 100.0
    checks_total: int = 0
    checks_failed: int = 0
    last_error: str | None = None


# ---------------------------------------------------------------------------
# Internal tracking structure (not a Pydantic model for performance)
# ---------------------------------------------------------------------------


class _TrackedBackend:
    """Internal mutable state for a monitored backend."""

    __slots__ = (
        "backend_type",
        "check_fn",
        "consecutive_failures",
        "history",
        "last_healthy",
        "latency_samples",
        "url",
    )

    def __init__(self, backend_type: str, url: str, check_fn: Any | None = None) -> None:
        self.backend_type = backend_type
        self.url = url
        self.check_fn = check_fn  # Optional custom check callable
        self.history: deque[HealthCheck] = deque(maxlen=_HISTORY_SIZE)
        self.consecutive_failures: int = 0
        self.last_healthy: datetime | None = None
        self.latency_samples: deque[float] = deque(maxlen=100)


# ---------------------------------------------------------------------------
# HealthMonitor
# ---------------------------------------------------------------------------


class HealthMonitor:
    """Continuous health monitoring for all registered backends.

    Usage::

        monitor = HealthMonitor(check_interval=10.0)
        await monitor.register_backend("ollama", "http://127.0.0.1:11434")
        await monitor.start()
        ...
        status = monitor.get_status("ollama")
        await monitor.stop()
    """

    def __init__(self, check_interval: float = 10.0) -> None:
        self._check_interval = check_interval
        self._backends: dict[str, _TrackedBackend] = {}
        self._running = False
        self._task: asyncio.Task[None] | None = None
        self._client: httpx.AsyncClient | None = None

    # -- lifecycle ----------------------------------------------------------

    async def start(self) -> None:
        """Start the background health check loop."""
        if self._running:
            logger.warning("HealthMonitor is already running")
            return

        self._client = httpx.AsyncClient(timeout=httpx.Timeout(5.0))
        self._running = True
        self._task = asyncio.create_task(self._check_loop(), name="health-monitor")
        logger.info("HealthMonitor started (interval=%.1fs, backends=%d)", self._check_interval, len(self._backends))

    async def stop(self) -> None:
        """Stop the background loop and clean up."""
        self._running = False

        if self._task is not None:
            self._task.cancel()
            with contextlib.suppress(asyncio.CancelledError):
                await self._task
            self._task = None

        if self._client is not None:
            await self._client.aclose()
            self._client = None

        logger.info("HealthMonitor stopped")

    @property
    def is_running(self) -> bool:
        """Return True if the background loop is active."""
        return self._running

    # -- backend registration -----------------------------------------------

    async def register_backend(
        self,
        backend_type: str,
        url: str,
        check_fn: Any | None = None,
    ) -> None:
        """Register a backend for monitoring.

        Parameters
        ----------
        backend_type:
            Identifier like ``"ollama"``, ``"openai"``, ``"anthropic"``.
        url:
            Base URL for the backend.
        check_fn:
            Optional async callable ``(httpx.AsyncClient, str) -> HealthCheck``.
            If not provided, a default check strategy is used based on
            ``backend_type``.
        """
        self._backends[backend_type] = _TrackedBackend(backend_type, url, check_fn)
        logger.info("Registered backend for health monitoring: %s at %s", backend_type, url)

    async def unregister_backend(self, backend_type: str) -> None:
        """Remove a backend from monitoring."""
        removed = self._backends.pop(backend_type, None)
        if removed:
            logger.info("Unregistered backend from health monitoring: %s", backend_type)

    # -- health checks ------------------------------------------------------

    async def check_backend(self, backend_type: str, url: str) -> HealthCheck:
        """Perform a single health check against a backend.

        Uses the registered custom check function if available, otherwise
        falls back to a default HTTP probe.
        """
        tracked = self._backends.get(backend_type)
        client = self._client or httpx.AsyncClient(timeout=httpx.Timeout(5.0))
        owns_client = self._client is None

        try:
            # Use custom check function if registered
            if tracked is not None and tracked.check_fn is not None:
                return cast("HealthCheck", await tracked.check_fn(client, url))

            # Default check strategies per backend type
            return await self._default_check(client, backend_type, url)
        finally:
            if owns_client:
                await client.aclose()

    async def _default_check(
        self,
        client: httpx.AsyncClient,
        backend_type: str,
        url: str,
    ) -> HealthCheck:
        """Run the default health check for a backend type."""
        start = time.perf_counter()
        now = datetime.now(tz=timezone.utc)

        try:
            if backend_type == "ollama":
                resp = await client.get(f"{url.rstrip('/')}/")
                latency = (time.perf_counter() - start) * 1000
                healthy = resp.status_code == 200
                details: dict[str, Any] = {"status_code": resp.status_code}
                # Try to get model count from tags endpoint
                if healthy:
                    try:
                        tags_resp = await client.get(f"{url.rstrip('/')}/api/tags")
                        if tags_resp.status_code == 200:
                            data = tags_resp.json()
                            details["model_count"] = len(data.get("models", []))
                    except Exception:
                        pass  # Non-critical, don't fail the check

                return HealthCheck(
                    backend_type=backend_type,
                    healthy=healthy,
                    latency_ms=latency,
                    checked_at=now,
                    details=details,
                )

            elif backend_type in ("openai", "openrouter"):
                # Lightweight probe: GET /v1/models (requires auth but we
                # just check connectivity -- a 401 still means the server is up)
                api_url = url.rstrip("/")
                resp = await client.get(f"{api_url}/models")
                latency = (time.perf_counter() - start) * 1000
                healthy = resp.status_code in (200, 401, 403)
                return HealthCheck(
                    backend_type=backend_type,
                    healthy=healthy,
                    latency_ms=latency,
                    checked_at=now,
                    details={"status_code": resp.status_code},
                    error=None if healthy else f"HTTP {resp.status_code}",
                )

            elif backend_type == "anthropic":
                # Anthropic doesn't have a /models endpoint; use a
                # lightweight POST that will 401 without a key but proves
                # the host is reachable.
                api_url = url.rstrip("/")
                resp = await client.post(
                    f"{api_url}/messages",
                    json={"model": "claude-3-haiku-20240307", "messages": [], "max_tokens": 1},
                    headers={"x-api-key": "health-check-probe", "anthropic-version": "2023-06-01"},
                )
                latency = (time.perf_counter() - start) * 1000
                # 401/400 = server is reachable, just auth/validation issue
                healthy = resp.status_code in (200, 400, 401, 403)
                return HealthCheck(
                    backend_type=backend_type,
                    healthy=healthy,
                    latency_ms=latency,
                    checked_at=now,
                    details={"status_code": resp.status_code},
                    error=None if healthy else f"HTTP {resp.status_code}",
                )

            else:
                # Generic HTTP GET probe
                resp = await client.get(url.rstrip("/"))
                latency = (time.perf_counter() - start) * 1000
                healthy = resp.status_code < 500
                return HealthCheck(
                    backend_type=backend_type,
                    healthy=healthy,
                    latency_ms=latency,
                    checked_at=now,
                    details={"status_code": resp.status_code},
                    error=None if healthy else f"HTTP {resp.status_code}",
                )

        except httpx.TimeoutException:
            latency = (time.perf_counter() - start) * 1000
            return HealthCheck(
                backend_type=backend_type,
                healthy=False,
                latency_ms=latency,
                checked_at=now,
                error="Timeout",
            )
        except httpx.ConnectError as exc:
            latency = (time.perf_counter() - start) * 1000
            return HealthCheck(
                backend_type=backend_type,
                healthy=False,
                latency_ms=latency,
                checked_at=now,
                error=f"Connection error: {exc}",
            )
        except Exception as exc:
            latency = (time.perf_counter() - start) * 1000
            return HealthCheck(
                backend_type=backend_type,
                healthy=False,
                latency_ms=latency,
                checked_at=now,
                error=f"{type(exc).__name__}: {exc}",
            )

    # -- status queries -----------------------------------------------------

    def get_status(self, backend_type: str) -> BackendHealth | None:
        """Get current health status for a backend."""
        tracked = self._backends.get(backend_type)
        if tracked is None:
            return None
        return self._build_health(tracked)

    def get_all_status(self) -> dict[str, BackendHealth]:
        """Get all backend health statuses."""
        return {bt: self._build_health(t) for bt, t in self._backends.items()}

    def is_healthy(self, backend_type: str) -> bool:
        """Quick check if backend is healthy."""
        tracked = self._backends.get(backend_type)
        if tracked is None:
            return False
        if not tracked.history:
            return False
        return tracked.history[-1].healthy

    # -- internal -----------------------------------------------------------

    def _build_health(self, tracked: _TrackedBackend) -> BackendHealth:
        """Build a BackendHealth snapshot from internal tracking state."""
        checks_total = len(tracked.history)
        checks_failed = sum(1 for c in tracked.history if not c.healthy)

        uptime_percent = (checks_total - checks_failed) / checks_total * 100.0 if checks_total > 0 else 100.0

        avg_latency = 0.0
        if tracked.latency_samples:
            avg_latency = sum(tracked.latency_samples) / len(tracked.latency_samples)

        last_check = tracked.history[-1].checked_at if tracked.history else datetime.now(tz=timezone.utc)
        is_healthy = tracked.history[-1].healthy if tracked.history else False
        last_error = tracked.history[-1].error if tracked.history and not tracked.history[-1].healthy else None

        return BackendHealth(
            backend_type=tracked.backend_type,
            url=tracked.url,
            is_healthy=is_healthy,
            last_check=last_check,
            last_healthy=tracked.last_healthy,
            consecutive_failures=tracked.consecutive_failures,
            avg_latency_ms=avg_latency,
            uptime_percent=uptime_percent,
            checks_total=checks_total,
            checks_failed=checks_failed,
            last_error=last_error,
        )

    def _record_check(self, tracked: _TrackedBackend, check: HealthCheck) -> None:
        """Record a health check result in the tracking state."""
        tracked.history.append(check)
        tracked.latency_samples.append(check.latency_ms)

        if check.healthy:
            tracked.consecutive_failures = 0
            tracked.last_healthy = check.checked_at
        else:
            tracked.consecutive_failures += 1
            if tracked.consecutive_failures >= 3:
                logger.warning(
                    "Backend %s has %d consecutive failures (last error: %s)",
                    tracked.backend_type,
                    tracked.consecutive_failures,
                    check.error,
                )

    async def _check_loop(self) -> None:
        """Background loop that runs health checks at the configured interval."""
        logger.debug("Health check loop started")
        try:
            while self._running:
                await self._run_all_checks()
                await asyncio.sleep(self._check_interval)
        except asyncio.CancelledError:
            logger.debug("Health check loop cancelled")
            raise

    async def _run_all_checks(self) -> None:
        """Run health checks for all registered backends concurrently."""
        if not self._backends:
            return

        tasks = [self._check_and_record(tracked) for tracked in self._backends.values()]
        await asyncio.gather(*tasks, return_exceptions=True)

    async def _check_and_record(self, tracked: _TrackedBackend) -> None:
        """Check a single backend and record the result."""
        try:
            check = await self.check_backend(tracked.backend_type, tracked.url)
            self._record_check(tracked, check)
            logger.debug(
                "Health check %s: healthy=%s latency=%.1fms",
                tracked.backend_type,
                check.healthy,
                check.latency_ms,
            )
        except Exception as exc:
            # Should not happen (check_backend catches exceptions) but be safe
            logger.error("Unexpected error checking %s: %s", tracked.backend_type, exc)
            error_check = HealthCheck(
                backend_type=tracked.backend_type,
                healthy=False,
                latency_ms=0.0,
                checked_at=datetime.now(tz=timezone.utc),
                error=f"Monitor error: {type(exc).__name__}: {exc}",
            )
            self._record_check(tracked, error_check)

    # -- repr ---------------------------------------------------------------

    def __repr__(self) -> str:
        return (
            f"HealthMonitor(interval={self._check_interval}s, backends={len(self._backends)}, running={self._running})"
        )
