"""Infrastructure readiness checks for downstream services."""

from __future__ import annotations

import os
import subprocess
from abc import ABC, abstractmethod
from typing import Any
from urllib.parse import urlparse

import httpx

from .models import InfraResult


class InfraCheck(ABC):
    """Base class for infrastructure readiness checks.

    Subclass this for checks that don't fit the built-in factory functions.

    Example::

        class MyCheck(InfraCheck):
            name = "My service"

            def check(self) -> InfraResult:
                # ... your logic ...
                return InfraResult(ok=True, detail="all good")
    """

    @property
    @abstractmethod
    def name(self) -> str: ...

    @abstractmethod
    def check(self) -> InfraResult: ...


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _parse_version(v: str) -> tuple[int, ...]:
    """Parse ``'v0.5.1'`` into ``(0, 5, 1)`` for comparison."""
    import re
    return tuple(int(m) for m in re.findall(r"\d+", v.strip().lstrip("v").split("-")[0]))


def _resolve_dotpath(data: Any, path: str) -> Any:
    """Traverse a nested dict with ``'a.b.c'`` notation.

    Raises ``KeyError`` if any segment is missing.
    """
    current = data
    for part in path.split("."):
        try:
            current = current[part]
        except (KeyError, TypeError, IndexError) as exc:
            raise KeyError(path) from exc
    return current


# ---------------------------------------------------------------------------
# http_health
# ---------------------------------------------------------------------------

class _HttpHealthCheck(InfraCheck):
    def __init__(
        self,
        url: str,
        *,
        _name: str | None,
        require: dict[str, Any] | None,
        min_version: str | None,
        timeout: float,
        hint: str,
    ) -> None:
        self._url = url
        self._name = _name or urlparse(url).netloc or url
        self._require = require
        self._min_version = min_version
        self._timeout = timeout
        self._hint = hint

    @property
    def name(self) -> str:
        return self._name

    def check(self) -> InfraResult:
        try:
            resp = httpx.get(self._url, timeout=self._timeout)
        except httpx.TimeoutException:
            return InfraResult(
                ok=False,
                detail=f"timed out after {self._timeout}s",
                hint=self._hint,
            )
        except Exception as exc:
            return InfraResult(ok=False, detail=str(exc), hint=self._hint)

        if resp.status_code >= 400:
            return InfraResult(
                ok=False,
                detail=f"HTTP {resp.status_code}",
                hint=self._hint,
            )

        try:
            data = resp.json()
        except Exception:
            return InfraResult(
                ok=False,
                detail="response is not valid JSON",
                hint=self._hint,
            )

        # Require specific fields/values
        if self._require:
            for path, expected in self._require.items():
                try:
                    actual = _resolve_dotpath(data, path)
                except KeyError:
                    return InfraResult(
                        ok=False,
                        detail=f"missing field: {path}",
                        hint=self._hint,
                    )
                if actual != expected:
                    return InfraResult(
                        ok=False,
                        detail=f"{path}={actual!r}, expected {expected!r}",
                        hint=self._hint,
                    )

        # Version comparison (warning, not error)
        version_detail = ""
        if self._min_version and isinstance(data, dict):
            reported = data.get("version")
            if reported is not None:
                version_detail = f"version={reported}"
                if _parse_version(str(reported)) < _parse_version(self._min_version):
                    return InfraResult(
                        ok=None,
                        detail=f"version {reported} < min {self._min_version}",
                        hint=self._hint,
                    )

        detail = version_detail or f"HTTP {resp.status_code}"
        return InfraResult(ok=True, detail=detail)


def http_health(
    url: str,
    *,
    name: str | None = None,
    require: dict[str, Any] | None = None,
    min_version: str | None = None,
    timeout: float = 5.0,
    hint: str = "",
) -> InfraCheck:
    """Check that an HTTP health endpoint is reachable and meets expectations.

    Args:
        url: Health endpoint URL.
        name: Display name (defaults to hostname from *url*).
        require: Dot-path field checks on the JSON response.
            Example: ``{"llm.configured": True}`` checks
            ``response["llm"]["configured"] == True``.
            Mismatches are **errors**.
        min_version: Minimum version string. Compared against the
            ``"version"`` key in the JSON response.
            Below minimum is a **warning**, not an error.
        timeout: Request timeout in seconds.
        hint: Remediation hint shown on failure/warning.
    """
    return _HttpHealthCheck(
        url, _name=name, require=require,
        min_version=min_version, timeout=timeout, hint=hint,
    )


# ---------------------------------------------------------------------------
# env_vars
# ---------------------------------------------------------------------------

class _EnvVarsCheck(InfraCheck):
    def __init__(self, names: tuple[str, ...], *, _name: str, hint: str) -> None:
        self._names = names
        self._name_str = _name
        self._hint = hint

    @property
    def name(self) -> str:
        return self._name_str

    def check(self) -> InfraResult:
        missing = [n for n in self._names if os.environ.get(n) is None]
        if missing:
            return InfraResult(
                ok=False,
                detail=f"missing: {', '.join(missing)}",
                hint=self._hint,
            )
        return InfraResult(ok=True, detail=f"{len(self._names)} vars present")


def env_vars(
    *names: str,
    name: str = "Env vars",
    hint: str = "",
) -> InfraCheck:
    """Check that required environment variables are set.

    Values are **not** leaked in the output — only presence is verified.

    Args:
        *names: One or more environment variable names.
        name: Display name for the check.
        hint: Remediation hint shown on failure.
    """
    return _EnvVarsCheck(names, _name=name, hint=hint)


# ---------------------------------------------------------------------------
# docker_health
# ---------------------------------------------------------------------------

class _DockerHealthCheck(InfraCheck):
    def __init__(self, container: str, *, _name: str, hint: str) -> None:
        self._container = container
        self._name_str = _name
        self._hint = hint

    @property
    def name(self) -> str:
        return self._name_str

    def check(self) -> InfraResult:
        try:
            r = subprocess.run(
                ["docker", "inspect", "--format", "{{.State.Status}}", self._container],
                capture_output=True, text=True, timeout=5,
            )
        except FileNotFoundError:
            return InfraResult(ok=False, detail="docker CLI not found", hint=self._hint)
        except subprocess.TimeoutExpired:
            return InfraResult(ok=False, detail="docker inspect timed out", hint=self._hint)

        if r.returncode != 0:
            return InfraResult(
                ok=False,
                detail=f"container {self._container!r} not found",
                hint=self._hint,
            )

        status = r.stdout.strip().lower()
        if status == "running":
            return InfraResult(ok=True, detail="running")
        return InfraResult(
            ok=False,
            detail=f"status: {status}",
            hint=self._hint,
        )


def docker_health(
    container: str,
    *,
    name: str | None = None,
    hint: str = "",
) -> InfraCheck:
    """Check that a Docker container is running.

    Args:
        container: Container name.
        name: Display name (defaults to ``"Docker <container>"``).
        hint: Remediation hint shown on failure.
    """
    return _DockerHealthCheck(
        container, _name=name or f"Docker {container}", hint=hint,
    )


# ---------------------------------------------------------------------------
# git_version
# ---------------------------------------------------------------------------

class _GitVersionCheck(InfraCheck):
    def __init__(
        self,
        repo_path: str,
        *,
        min_tag: str | None,
        _name: str,
        hint: str,
    ) -> None:
        self._repo_path = repo_path
        self._min_tag = min_tag
        self._name_str = _name
        self._hint = hint

    @property
    def name(self) -> str:
        return self._name_str

    def check(self) -> InfraResult:
        # Verify the path is a git repo
        try:
            r = subprocess.run(
                ["git", "-C", self._repo_path, "rev-parse", "--git-dir"],
                capture_output=True, text=True, timeout=5,
            )
        except FileNotFoundError:
            return InfraResult(ok=False, detail="git CLI not found", hint=self._hint)
        except subprocess.TimeoutExpired:
            return InfraResult(ok=False, detail="git command timed out", hint=self._hint)

        if r.returncode != 0:
            return InfraResult(
                ok=False,
                detail=f"not a git repo: {self._repo_path}",
                hint=self._hint,
            )

        if not self._min_tag:
            return InfraResult(ok=True, detail=self._repo_path)

        # Get current tag
        r = subprocess.run(
            ["git", "-C", self._repo_path, "describe", "--tags", "--abbrev=0"],
            capture_output=True, text=True, timeout=5,
        )
        if r.returncode != 0:
            return InfraResult(
                ok=None,
                detail=f"no tags found; cannot verify min_tag={self._min_tag}",
                hint=self._hint,
            )

        current_tag = r.stdout.strip()
        if _parse_version(current_tag) < _parse_version(self._min_tag):
            return InfraResult(
                ok=None,
                detail=f"{current_tag} < min {self._min_tag}",
                hint=self._hint,
            )

        return InfraResult(ok=True, detail=f"tag={current_tag}")


def git_version(
    repo_path: str,
    *,
    min_tag: str | None = None,
    name: str | None = None,
    hint: str = "",
) -> InfraCheck:
    """Check a local git repo's version against a minimum tag.

    Below-minimum versions are **warnings**, not errors.
    A missing repo or missing git CLI is an **error**.

    Args:
        repo_path: Path to the git repository.
        min_tag: Minimum tag version (e.g. ``"v0.5.0"``).
        name: Display name (defaults to ``"Git <basename>"``).
        hint: Remediation hint shown on failure/warning.
    """
    default_name = f"Git {os.path.basename(os.path.abspath(repo_path))}"
    return _GitVersionCheck(
        repo_path, min_tag=min_tag, _name=name or default_name, hint=hint,
    )
