"""Platform-aware dependency detection and installation."""

from __future__ import annotations

import platform
import subprocess
import sys
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any

import questionary

from ilum.constants import MIN_DOCKER_VERSION, MIN_HELM_VERSION, MIN_KUBECTL_VERSION
from ilum.doctor.checks import CheckResult, CheckStatus, check_binary
from ilum.errors import PrerequisiteError


def _ask_or_abort(question: questionary.Question) -> Any:
    """Run prompt; re-raise Ctrl+C instead of returning None."""
    result = question.ask()
    if result is None:
        raise KeyboardInterrupt
    return result


if TYPE_CHECKING:
    from ilum.cli.output import IlumConsole


@dataclass(frozen=True)
class ToolDef:
    """Metadata for a tool that the CLI may need."""

    name: str
    min_version: tuple[int, int] | None
    category: str  # "core" or "provider"
    manual_url: str


TOOL_REGISTRY: dict[str, ToolDef] = {
    "helm": ToolDef("helm", MIN_HELM_VERSION, "core", "https://helm.sh/docs/intro/install/"),
    "kubectl": ToolDef(
        "kubectl", MIN_KUBECTL_VERSION, "core", "https://kubernetes.io/docs/tasks/tools/"
    ),
    "docker": ToolDef("docker", MIN_DOCKER_VERSION, "core", "https://docs.docker.com/get-docker/"),
    "minikube": ToolDef("minikube", None, "provider", "https://minikube.sigs.k8s.io/docs/start/"),
    "k3d": ToolDef("k3d", None, "provider", "https://k3d.io/"),
    "kind": ToolDef(
        "kind",
        None,
        "provider",
        "https://kind.sigs.k8s.io/docs/user/quick-start/#installation",
    ),
}

# Process-lifetime cache to avoid repeated binary checks.
_verified_tools: set[str] = set()


def ensure_tool(
    name: str,
    console: IlumConsole,
    *,
    interactive: bool | None = None,
) -> None:
    """Check that *name* is available; offer to install if missing.

    Raises :class:`PrerequisiteError` if the tool remains unavailable.
    """
    if name in _verified_tools:
        return

    tool = TOOL_REGISTRY.get(name)
    if tool is None:
        raise PrerequisiteError(
            f"Unknown tool: {name}",
            suggestion="This is a bug — please report it.",
        )

    result = check_binary(name, tool.min_version)
    if result.status == CheckStatus.OK:
        _verified_tools.add(name)
        return

    # Tool is missing or outdated.
    if interactive is None:
        interactive = sys.stdin.isatty()

    if not interactive:
        console.warning(result.message)
        raise PrerequisiteError(
            f"{name} is required but not available",
            suggestion=f"Install manually: {tool.manual_url}",
        )

    # Interactive mode — offer to install.
    console.warning(result.message)
    if not _ask_or_abort(questionary.confirm(f"Install {name} now?", default=False)):
        raise PrerequisiteError(
            f"{name} is required but not available",
            suggestion=f"Install manually: {tool.manual_url}",
        )

    plat = _detect_platform()
    try:
        _install_tool(name, plat, console)
    except (subprocess.CalledProcessError, OSError, PrerequisiteError) as exc:
        console.error(f"Failed to install {name}: {exc}")
        raise PrerequisiteError(
            f"Automatic installation of {name} failed",
            suggestion=f"Install manually: {tool.manual_url}",
        ) from exc

    # Re-check after install.
    result = check_binary(name, tool.min_version)
    if result.status != CheckStatus.OK:
        raise PrerequisiteError(
            f"{name} was installed but version check failed",
            suggestion=f"Install manually: {tool.manual_url}",
        )

    console.success(f"{name} installed successfully")
    _verified_tools.add(name)


def ensure_tools(
    names: list[str],
    console: IlumConsole,
    *,
    interactive: bool | None = None,
) -> None:
    """Check multiple tools. Calls :func:`ensure_tool` for each."""
    for name in names:
        ensure_tool(name, console, interactive=interactive)


# ---------------------------------------------------------------------------
# ToolInstaller — kept for backward compat with wizard/flow.py & quickstart
# ---------------------------------------------------------------------------


class ToolInstaller:
    """Detect missing tools and offer to install them."""

    def check_and_install(
        self,
        console: IlumConsole,
        *,
        non_interactive: bool = False,
    ) -> list[CheckResult]:
        """Run binary checks; prompt to install any that are missing.

        Returns the final list of check results after any installation attempts.
        """
        results: list[CheckResult] = []
        plat = _detect_platform()

        for tool in TOOL_REGISTRY.values():
            if tool.category != "core":
                continue

            result = check_binary(tool.name, tool.min_version)
            if result.status == CheckStatus.OK:
                console.success(result.message)
                results.append(result)
                continue

            console.warning(result.message)

            if non_interactive:
                results.append(result)
                continue

            if _ask_or_abort(questionary.confirm(f"Install {tool.name} now?", default=False)):
                try:
                    _install_tool(tool.name, plat, console)
                    # Re-check after install
                    result = check_binary(tool.name, tool.min_version)
                    if result.status == CheckStatus.OK:
                        console.success(f"{tool.name} installed successfully")
                    else:
                        console.warning(f"{tool.name} installed but version check failed")
                except (subprocess.CalledProcessError, OSError, PrerequisiteError) as exc:
                    console.error(f"Failed to install {tool.name}: {exc}")

            results.append(result)

        return results

    # Keep static helper accessible for tests that reference it.
    @staticmethod
    def _detect_platform() -> str:  # pragma: no cover
        return _detect_platform()

    # Legacy instance methods delegate to module-level helpers.
    def _install_helm(self, plat: str, console: IlumConsole) -> None:
        _install_helm(plat, console)

    def _install_kubectl(self, plat: str, console: IlumConsole) -> None:
        _install_kubectl(plat, console)


# ---------------------------------------------------------------------------
# Platform detection
# ---------------------------------------------------------------------------


def _detect_platform() -> str:
    """Return 'darwin', 'windows', or 'linux'."""
    system = platform.system().lower()
    if system == "darwin":
        return "darwin"
    if system == "windows":
        return "windows"
    return "linux"


# ---------------------------------------------------------------------------
# Tool installation dispatch
# ---------------------------------------------------------------------------


def _install_tool(name: str, plat: str, console: IlumConsole) -> None:
    """Dispatch to the correct installer for *name*."""
    installers = {
        "helm": _install_helm,
        "kubectl": _install_kubectl,
        "docker": _install_docker,
        "minikube": _install_minikube,
        "k3d": _install_k3d,
        "kind": _install_kind,
    }
    fn = installers.get(name)
    if fn is None:
        raise PrerequisiteError(f"No automatic installer for {name}")
    fn(plat, console)


# ---------------------------------------------------------------------------
# Per-tool installers
# ---------------------------------------------------------------------------


def _install_helm(plat: str, console: IlumConsole) -> None:
    """Install Helm via Homebrew, winget, or the official install script."""
    if plat == "darwin":
        console.info("Installing Helm via Homebrew...")
        subprocess.run(["brew", "install", "helm"], check=True)
    elif plat == "windows":
        console.info("Installing Helm via winget...")
        subprocess.run(
            [
                "winget",
                "install",
                "--id",
                "Helm.Helm",
                "-e",
                "--accept-source-agreements",
                "--accept-package-agreements",
            ],
            check=True,
        )
    else:
        console.info("Installing Helm via official install script...")
        script_url = "https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3"
        subprocess.run(
            ["bash", "-c", f"curl -fsSL {script_url} | bash"],
            check=True,
        )


def _install_kubectl(plat: str, console: IlumConsole) -> None:
    """Install kubectl via Homebrew, winget, or direct binary download."""
    if plat == "darwin":
        console.info("Installing kubectl via Homebrew...")
        subprocess.run(["brew", "install", "kubectl"], check=True)
    elif plat == "windows":
        console.info("Installing kubectl via winget...")
        subprocess.run(
            [
                "winget",
                "install",
                "--id",
                "Kubernetes.kubectl",
                "-e",
                "--accept-source-agreements",
                "--accept-package-agreements",
            ],
            check=True,
        )
    else:
        console.info("Installing kubectl via direct download...")
        arch = platform.machine()
        if arch == "x86_64":
            arch = "amd64"
        elif arch in ("aarch64", "arm64"):
            arch = "arm64"

        cmds = (
            f"curl -fsSLo /tmp/kubectl "
            f'"https://dl.k8s.io/release/$(curl -fsSL https://dl.k8s.io/release/stable.txt)/bin/linux/{arch}/kubectl"'
            " && chmod +x /tmp/kubectl"
            " && sudo mv /tmp/kubectl /usr/local/bin/kubectl"
        )
        subprocess.run(["bash", "-c", cmds], check=True)


def _install_docker(plat: str, console: IlumConsole) -> None:
    """Install Docker via Homebrew cask, winget, or the official get-docker script."""
    if plat == "darwin":
        console.info("Installing Docker via Homebrew...")
        subprocess.run(["brew", "install", "--cask", "docker"], check=True)
        _start_docker_desktop_darwin(console)
    elif plat == "windows":
        console.info("Installing Docker via winget...")
        subprocess.run(
            [
                "winget",
                "install",
                "--id",
                "Docker.DockerDesktop",
                "-e",
                "--accept-source-agreements",
                "--accept-package-agreements",
            ],
            check=True,
        )
        console.warning(
            "Docker Desktop may require a system restart. "
            "Please launch Docker Desktop before continuing."
        )
    else:
        console.info("Installing Docker via official install script (may require sudo)...")
        subprocess.run(
            ["bash", "-c", "curl -fsSL https://get.docker.com | sh"],
            check=True,
        )
        _post_install_docker_linux(console)


def _has_passwordless_sudo() -> bool:
    """Return True if the current user can run sudo without a password."""
    result = subprocess.run(
        ["sudo", "-n", "true"],
        capture_output=True,
    )
    return result.returncode == 0


def _post_install_docker_linux(console: IlumConsole) -> None:
    """Add current user to the docker group and start the daemon."""
    import os

    if not _has_passwordless_sudo():
        console.warning(
            "Passwordless sudo is not available. "
            "Please run these commands manually to finish Docker setup:\n"
            "  sudo usermod -aG docker $USER\n"
            "  sudo systemctl start docker\n"
            "  newgrp docker   # or log out and back in"
        )
        return

    user = os.environ.get("USER") or os.environ.get("LOGNAME", "")
    if user:
        console.info(f"Adding user '{user}' to the docker group...")
        result = subprocess.run(
            ["sudo", "usermod", "-aG", "docker", user],
            capture_output=True,
            text=True,
        )
        if result.returncode != 0:
            console.warning(
                f"Failed to add user to docker group: {result.stderr.strip()}\n"
                "Run manually: sudo usermod -aG docker $USER"
            )
    # Start the daemon so the socket is available now.
    subprocess.run(
        ["sudo", "systemctl", "start", "docker"],
        capture_output=True,
    )
    console.warning(
        "You may need to log out and back in (or run 'newgrp docker') "
        "for Docker group permissions to take effect."
    )


_DOCKER_READY_TIMEOUT = 60  # seconds


def _start_docker_desktop_darwin(console: IlumConsole) -> None:
    """Launch Docker Desktop on macOS and wait for the daemon to be ready."""
    import time

    console.info("Starting Docker Desktop...")
    subprocess.run(["open", "-a", "Docker"], check=False)
    console.info("Waiting for Docker daemon to be ready (this may take a minute)...")
    deadline = time.monotonic() + _DOCKER_READY_TIMEOUT
    while time.monotonic() < deadline:
        result = subprocess.run(
            ["docker", "info"],
            capture_output=True,
            text=True,
        )
        if result.returncode == 0:
            console.success("Docker daemon is ready.")
            return
        time.sleep(3)
    console.warning(
        f"Docker daemon did not become ready within {_DOCKER_READY_TIMEOUT}s. "
        "Please ensure Docker Desktop is running before continuing."
    )


def _install_minikube(plat: str, console: IlumConsole) -> None:
    """Install minikube via Homebrew, winget, or direct binary download."""
    if plat == "darwin":
        console.info("Installing minikube via Homebrew...")
        subprocess.run(["brew", "install", "minikube"], check=True)
    elif plat == "windows":
        console.info("Installing minikube via winget...")
        subprocess.run(
            [
                "winget",
                "install",
                "--id",
                "Kubernetes.minikube",
                "-e",
                "--accept-source-agreements",
                "--accept-package-agreements",
            ],
            check=True,
        )
    else:
        console.info("Installing minikube via direct download...")
        arch = platform.machine()
        if arch == "x86_64":
            arch = "amd64"
        elif arch in ("aarch64", "arm64"):
            arch = "arm64"

        cmds = (
            f"curl -fsSLo /tmp/minikube "
            f"https://storage.googleapis.com/minikube/releases/latest/minikube-linux-{arch}"
            " && chmod +x /tmp/minikube"
            " && sudo mv /tmp/minikube /usr/local/bin/minikube"
        )
        subprocess.run(["bash", "-c", cmds], check=True)


def _install_k3d(plat: str, console: IlumConsole) -> None:
    """Install k3d via Homebrew, winget, or the official install script."""
    if plat == "darwin":
        console.info("Installing k3d via Homebrew...")
        subprocess.run(["brew", "install", "k3d"], check=True)
    elif plat == "windows":
        console.info("Installing k3d via winget...")
        subprocess.run(
            [
                "winget",
                "install",
                "--id",
                "k3d-io.k3d",
                "-e",
                "--accept-source-agreements",
                "--accept-package-agreements",
            ],
            check=True,
        )
    else:
        console.info("Installing k3d via official install script...")
        subprocess.run(
            [
                "bash",
                "-c",
                "curl -fsSL https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh | bash",
            ],
            check=True,
        )


def _install_kind(plat: str, console: IlumConsole) -> None:
    """Install kind via Homebrew, winget, or direct binary download."""
    if plat == "darwin":
        console.info("Installing kind via Homebrew...")
        subprocess.run(["brew", "install", "kind"], check=True)
    elif plat == "windows":
        console.info("Installing kind via winget...")
        subprocess.run(
            [
                "winget",
                "install",
                "--id",
                "Kubernetes.kind",
                "-e",
                "--accept-source-agreements",
                "--accept-package-agreements",
            ],
            check=True,
        )
    else:
        console.info("Installing kind via direct download...")
        arch = platform.machine()
        if arch == "x86_64":
            arch = "amd64"
        elif arch in ("aarch64", "arm64"):
            arch = "arm64"

        cmds = (
            f"curl -fsSLo /tmp/kind "
            f"https://kind.sigs.k8s.io/dl/latest/kind-linux-{arch}"
            " && chmod +x /tmp/kind"
            " && sudo mv /tmp/kind /usr/local/bin/kind"
        )
        subprocess.run(["bash", "-c", cmds], check=True)
