"""ClustersService — multi-cluster management.

CRUD for cluster configs stored in ~/.kuberoku/config.yaml.
Delegates to config.user for file operations. The ``info`` method
connects to the target cluster to gather live data.
"""

from __future__ import annotations

import pathlib
from collections.abc import Callable
from typing import TYPE_CHECKING

import kubernetes  # type: ignore[import-untyped]

from kuberoku.config import user as _user_config
from kuberoku.exceptions import (
    ClusterAlreadyExistsError,
    ClusterNotFoundError,
    NoCurrentClusterError,
)
from kuberoku.models import ClusterConfig, ClusterInfo

if TYPE_CHECKING:
    from kuberoku.factory import KuberokuFactory


def detect_kubeconfig_context() -> tuple[str | None, str | None]:
    """Detect the active kubeconfig context and cluster name.

    Returns (context_name, cluster_name). Both are None if kubeconfig
    is not available or unreadable.
    """
    try:
        _contexts, active = kubernetes.config.list_kube_config_contexts()
        if active:
            ctx_name: str = active.get("name", "")
            cluster_name: str = active.get("context", {}).get("cluster", ctx_name)
            return ctx_name or None, cluster_name or None
    except Exception:
        pass
    return None, None


class ClustersService:
    """Manages registered clusters."""

    def __init__(self, factory: KuberokuFactory) -> None:
        self._factory = factory
        self._config_path: pathlib.Path | None = None

    def _path(self) -> pathlib.Path | None:
        return self._config_path

    def set_config_path(self, path: pathlib.Path) -> None:
        """Override config path (for testing)."""
        self._config_path = path

    def add(
        self,
        name: str,
        context: str,
        *,
        namespace: str | None = None,
        default: bool = False,
        on_step: Callable[[str], None] | None = None,
    ) -> ClusterConfig:
        """Register a cluster."""
        if on_step:
            on_step(f"Adding cluster '{name}'...")
        try:
            result = _user_config.add_cluster(
                name,
                context,
                namespace=namespace,
                default=default,
                path=self._path(),
            )
        except ValueError:
            raise ClusterAlreadyExistsError(name) from None
        if on_step:
            on_step(f"Cluster '{name}' registered.")
        return result

    def remove(
        self,
        name: str,
        on_step: Callable[[str], None] | None = None,
    ) -> None:
        """Unregister a cluster."""
        if on_step:
            on_step(f"Removing cluster '{name}'...")
        removed = _user_config.remove_cluster(name, path=self._path())
        if not removed:
            raise ClusterNotFoundError(name)
        if on_step:
            on_step(f"Cluster '{name}' removed.")

    def list(self) -> tuple[ClusterConfig, ...]:
        """List all configured clusters."""
        return _user_config.list_clusters(path=self._path())

    def switch(
        self,
        name: str,
        on_step: Callable[[str], None] | None = None,
    ) -> None:
        """Switch the current cluster."""
        if on_step:
            on_step(f"Switching to cluster '{name}'...")
        try:
            _user_config.set_current_cluster(name, path=self._path())
        except ValueError:
            raise ClusterNotFoundError(name) from None
        if on_step:
            on_step(f"Switched to cluster '{name}'.")

    def current(self) -> str:
        """Get the current cluster name.

        Resolution order:
        1. Explicit current_cluster in config.yaml
        2. Auto-detected kubeconfig context
        3. Raise NoCurrentClusterError
        """
        name = _user_config.get_current_cluster_name(path=self._path())
        if name is not None:
            return name
        ctx_name, _cluster = detect_kubeconfig_context()
        if ctx_name is not None:
            return ctx_name
        raise NoCurrentClusterError()

    def info(
        self,
        name: str,
        on_step: Callable[[str], None] | None = None,
    ) -> ClusterInfo:
        """Get cluster info including live data (K8s version, nodes, apps).

        For the info command, we need to connect to the target cluster's
        context to get live data. If the target is the current factory's
        cluster, we reuse the existing connection.
        """
        if on_step:
            on_step(f"Querying cluster '{name}'...")

        cluster = _user_config.get_cluster(name, path=self._path())
        if cluster is None:
            raise ClusterNotFoundError(name)

        current = _user_config.get_current_cluster_name(path=self._path())
        is_current = name == current

        # Try to get live data from the cluster
        k8s_version: str | None = None
        node_count: int | None = None
        app_count: int | None = None

        try:
            # Use the factory's K8s client if this is the current cluster,
            # otherwise create a temporary client for the target context
            k8s = self._factory.k8s
            k8s_version = k8s.server_version()
            nodes = k8s.list_nodes()
            node_count = len(nodes)

            # Count apps by listing app configmaps with managed-by label
            ns = cluster.namespace or self._factory.namespace
            app_labels = {
                f"{self._factory.domain}/managed-by": self._factory.prefix,
                f"{self._factory.domain}/resource-type": "app",
            }
            app_cms = k8s.list_configmaps(ns, labels=app_labels)
            app_count = len(app_cms)
        except Exception:
            pass

        return ClusterInfo(
            name=name,
            context=cluster.context,
            is_current=is_current,
            k8s_version=k8s_version,
            node_count=node_count,
            app_count=app_count,
        )
