"""Local Kubernetes cluster lifecycle management."""

from __future__ import annotations

import json
import platform
import shutil
import subprocess
from dataclasses import dataclass
from datetime import UTC, datetime
from enum import Enum
from typing import TYPE_CHECKING

from ilum.config.models import ClusterRecord
from ilum.errors import PrerequisiteError
from ilum.wizard.deps import ensure_tool

if TYPE_CHECKING:
    from ilum.cli.output import IlumConsole


@dataclass(frozen=True)
class DiscoveredCluster:
    """A cluster discovered from a local provider binary."""

    name: str
    provider: str
    kubecontext: str
    status: str  # "Running", "Stopped", ""


def _user_in_docker_group() -> bool:
    """Return True if the current user is a member of the ``docker`` group.

    Checks ``/etc/group`` directly rather than the process's live group
    list, because ``usermod -aG`` updates the file but the current
    session won't reflect the change until re-login.
    """
    import grp
    import os

    try:
        docker_grp = grp.getgrnam("docker")
    except KeyError:
        return False
    user = os.environ.get("USER") or os.environ.get("LOGNAME", "")
    return user in docker_grp.gr_mem


def _run_with_docker_group(cmd: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
    """Run *cmd* and, on Linux, retry under ``sg docker`` if it fails.

    After a fresh Docker install the current shell doesn't have the
    ``docker`` group yet.  ``sg docker -c "…"`` executes a command as
    if the user had that supplementary group, avoiding a logout/login.

    ``sg`` only works without a password prompt if the user is already
    listed in ``/etc/group`` for the ``docker`` group.  If they aren't,
    we re-raise the original error with guidance instead of triggering
    an interactive password prompt.
    """
    try:
        return subprocess.run(cmd, **kwargs)  # type: ignore[arg-type]
    except subprocess.CalledProcessError:
        if platform.system().lower() != "linux":
            raise
        if not _user_in_docker_group():
            raise PrerequisiteError(
                "Docker permission denied: current user is not in the 'docker' group",
                suggestion=(
                    "Run 'sudo usermod -aG docker $USER' and then log out and back in, "
                    "or run 'newgrp docker' in your shell before retrying."
                ),
            )
        # User is in the group on disk but not in the current session —
        # ``sg docker`` will activate it without a password prompt.
        shell_cmd = " ".join(cmd)
        return subprocess.run(  # type: ignore[arg-type]
            ["sg", "docker", "-c", shell_cmd],
            **kwargs,
        )


class ClusterProvider(Enum):
    MINIKUBE = "minikube"
    K3D = "k3d"
    KIND = "kind"


@dataclass(frozen=True)
class ClusterPreset:
    cpus: int
    memory: str  # e.g. "12g"
    name: str


PRESETS: dict[str, ClusterPreset] = {
    "dev": ClusterPreset(cpus=6, memory="12g", name="ilum-dev"),
    "full": ClusterPreset(cpus=8, memory="18g", name="ilum"),
}


class ClusterManager:
    """Create, delete, and list local Kubernetes clusters."""

    def create(
        self,
        provider: ClusterProvider,
        preset: ClusterPreset,
        console: IlumConsole,
        *,
        name: str = "",
    ) -> ClusterRecord:
        """Create a cluster and return a record with the kubecontext name."""
        binary = provider.value
        ensure_tool(binary, console)

        cluster_name = name or preset.name

        console.info(f"Creating {provider.value} cluster '{cluster_name}'...")

        if provider is ClusterProvider.MINIKUBE:
            kubecontext = self._create_minikube(cluster_name, preset)
        elif provider is ClusterProvider.K3D:
            kubecontext = self._create_k3d(cluster_name, preset)
        elif provider is ClusterProvider.KIND:
            kubecontext = self._create_kind(cluster_name, preset)
        else:  # pragma: no cover
            raise PrerequisiteError(f"Unsupported provider: {provider.value}")

        console.success(f"Cluster '{cluster_name}' created (context: {kubecontext})")

        return ClusterRecord(
            name=cluster_name,
            provider=provider.value,
            kubecontext=kubecontext,
            created_at=datetime.now(UTC).isoformat(),
        )

    def delete(
        self,
        provider: ClusterProvider,
        name: str,
        console: IlumConsole,
    ) -> None:
        """Delete a local cluster."""
        binary = provider.value
        if shutil.which(binary) is None:
            raise PrerequisiteError(f"{binary} not found on PATH")

        console.info(f"Deleting {provider.value} cluster '{name}'...")

        if provider is ClusterProvider.MINIKUBE:
            subprocess.run(
                ["minikube", "delete", "--profile", name],
                check=True,
                capture_output=True,
                text=True,
            )
        elif provider is ClusterProvider.K3D:
            subprocess.run(
                ["k3d", "cluster", "delete", name],
                check=True,
                capture_output=True,
                text=True,
            )
        elif provider is ClusterProvider.KIND:
            subprocess.run(
                ["kind", "delete", "cluster", "--name", name],
                check=True,
                capture_output=True,
                text=True,
            )

        console.success(f"Cluster '{name}' deleted")

    @staticmethod
    def list_local(records: list[ClusterRecord]) -> list[ClusterRecord]:
        """Return merged list of managed (config) and discovered (live) clusters."""
        # Tag managed records
        managed: list[ClusterRecord] = [r.model_copy(update={"source": "managed"}) for r in records]
        managed_keys: set[tuple[str, str]] = {(r.name, r.provider) for r in managed}

        # Discover live clusters from provider binaries
        discovered = discover_clusters()

        merged = list(managed)
        for dc in discovered:
            key = (dc.name, dc.provider)
            if key in managed_keys:
                # Enrich existing managed record with live status
                merged = [
                    r.model_copy(update={"status": dc.status}) if (r.name, r.provider) == key else r
                    for r in merged
                ]
            else:
                merged.append(
                    ClusterRecord(
                        name=dc.name,
                        provider=dc.provider,
                        kubecontext=dc.kubecontext,
                        source="detected",
                        status=dc.status,
                    )
                )
        return merged

    # -- Provider-specific creation ------------------------------------------

    @staticmethod
    def _create_minikube(name: str, preset: ClusterPreset) -> str:
        _run_with_docker_group(
            [
                "minikube",
                "start",
                "--profile",
                name,
                "--cpus",
                str(preset.cpus),
                "--memory",
                preset.memory,
                "--driver",
                "docker",
            ],
            check=True,
            capture_output=True,
            text=True,
        )
        return name

    @staticmethod
    def _create_k3d(name: str, preset: ClusterPreset) -> str:
        # k3d memory is in MB; convert e.g. "8g" → "8192m"
        mem_value = preset.memory.rstrip("gGmM")
        if preset.memory.lower().endswith("g"):
            mem_value = str(int(mem_value) * 1024)
        # k3d uses server args for resource limits
        _run_with_docker_group(
            [
                "k3d",
                "cluster",
                "create",
                name,
                "--servers",
                "1",
                "--agents",
                "0",
            ],
            check=True,
            capture_output=True,
            text=True,
        )
        return f"k3d-{name}"

    @staticmethod
    def _create_kind(name: str, preset: ClusterPreset) -> str:
        _run_with_docker_group(
            [
                "kind",
                "create",
                "cluster",
                "--name",
                name,
            ],
            check=True,
            capture_output=True,
            text=True,
        )
        return f"kind-{name}"


# -- Cluster discovery functions -----------------------------------------------


def _discover_minikube() -> list[DiscoveredCluster]:
    """Discover minikube clusters via ``minikube profile list -o json``."""
    if shutil.which("minikube") is None:
        return []
    try:
        result = subprocess.run(
            ["minikube", "profile", "list", "-o", "json"],
            capture_output=True,
            text=True,
            timeout=10,
        )
        # minikube may exit non-zero but still produce valid JSON on stdout
        if not result.stdout.strip():
            return []
        data = json.loads(result.stdout)
        profiles = data.get("valid", [])
        return [
            DiscoveredCluster(
                name=p["Name"],
                provider="minikube",
                kubecontext=p["Name"],
                status="Running" if p.get("Status") == "OK" else "Stopped",
            )
            for p in profiles
            if "Name" in p
        ]
    except Exception:  # noqa: BLE001
        return []


def _discover_k3d() -> list[DiscoveredCluster]:
    """Discover k3d clusters via ``k3d cluster list -o json``."""
    if shutil.which("k3d") is None:
        return []
    try:
        result = subprocess.run(
            ["k3d", "cluster", "list", "-o", "json"],
            capture_output=True,
            text=True,
            timeout=10,
            check=True,
        )
        clusters = json.loads(result.stdout)
        return [
            DiscoveredCluster(
                name=c["name"],
                provider="k3d",
                kubecontext=f"k3d-{c['name']}",
                status="Running" if c.get("serversRunning", 0) > 0 else "Stopped",
            )
            for c in clusters
            if "name" in c
        ]
    except Exception:  # noqa: BLE001
        return []


def _discover_kind() -> list[DiscoveredCluster]:
    """Discover kind clusters via ``kind get clusters``."""
    if shutil.which("kind") is None:
        return []
    try:
        result = subprocess.run(
            ["kind", "get", "clusters"],
            capture_output=True,
            text=True,
            timeout=10,
            check=True,
        )
        names = [line.strip() for line in result.stdout.splitlines() if line.strip()]
        return [
            DiscoveredCluster(
                name=n,
                provider="kind",
                kubecontext=f"kind-{n}",
                status="Running",
            )
            for n in names
        ]
    except Exception:  # noqa: BLE001
        return []


def discover_clusters() -> list[DiscoveredCluster]:
    """Discover clusters from all supported local providers (best-effort)."""
    return _discover_minikube() + _discover_k3d() + _discover_kind()
