"""Thin wrapper around the kubernetes-client/python library."""

from __future__ import annotations

from collections.abc import Iterator
from dataclasses import dataclass
from typing import Any

from kubernetes.client import (
    ApiClient,
    AuthorizationV1Api,
    BatchV1Api,
    CoreV1Api,
    VersionApi,
)
from kubernetes.client.exceptions import ApiException
from kubernetes.client.models import (
    V1Pod,
    V1ResourceAttributes,
    V1SelfSubjectAccessReview,
    V1SelfSubjectAccessReviewSpec,
)
from kubernetes.config import (
    ConfigException,
    list_kube_config_contexts,
    load_incluster_config,
    load_kube_config,
    new_client_from_config,
)

from ilum.errors import ClusterConnectionError

_DEFAULT_VERBS: list[str] = ["get", "list", "create", "update", "delete"]
_DEFAULT_RESOURCES: list[str] = ["pods", "services", "deployments", "configmaps", "secrets"]


@dataclass
class PodStatus:
    """Status summary for a single Kubernetes pod."""

    name: str
    namespace: str
    phase: str  # Running, Pending, etc.
    ready: bool
    restart_count: int
    message: str = ""
    containers: tuple[str, ...] = ()


@dataclass
class PVCStatus:
    """Status summary for a single PersistentVolumeClaim."""

    name: str
    namespace: str
    phase: str  # Bound, Pending, etc.
    storage_class: str = ""
    capacity: str = ""


@dataclass(frozen=True)
class EventInfo:
    """Summary of a single Kubernetes event."""

    name: str
    namespace: str
    reason: str
    message: str
    event_type: str  # Normal, Warning
    involved_object: str  # "Pod/ilum-core-abc"
    count: int
    first_seen: str
    last_seen: str
    source: str


@dataclass(frozen=True)
class PodMetrics:
    """Resource usage for a single pod."""

    name: str
    namespace: str
    cpu_millicores: int
    memory_bytes: int


@dataclass(frozen=True)
class PodResources:
    """Resource requests and limits for a single pod."""

    name: str
    cpu_request_millicores: int
    cpu_limit_millicores: int
    memory_request_bytes: int
    memory_limit_bytes: int


class KubeClient:
    """Lazy Kubernetes API client with graceful error handling."""

    def __init__(self, kubecontext: str = "") -> None:
        self._kubecontext = kubecontext
        self._client: ApiClient | None = None
        self._core: CoreV1Api | None = None
        self._auth: AuthorizationV1Api | None = None
        self._version: VersionApi | None = None
        self._batch: BatchV1Api | None = None

    # ------------------------------------------------------------------
    # Lazy API accessors
    # ------------------------------------------------------------------

    @property
    def _api_client(self) -> ApiClient:
        """Lazily create and cache a :class:`kubernetes.client.ApiClient`."""
        if self._client is None:
            try:
                if self._kubecontext:
                    self._client = new_client_from_config(context=self._kubecontext)
                else:
                    load_kube_config()
                    self._client = ApiClient()
            except ConfigException:
                try:
                    load_incluster_config()
                    self._client = ApiClient()
                except ConfigException as exc:
                    raise ClusterConnectionError(
                        f"Failed to load Kubernetes config: {exc}",
                        suggestion=(
                            "Ensure a valid kubeconfig exists at ~/.kube/config "
                            "or set the KUBECONFIG environment variable."
                        ),
                        error_code="ILUM-010",
                    ) from exc
        return self._client

    @property
    def _core_v1(self) -> CoreV1Api:
        """Lazily create and cache a :class:`CoreV1Api`."""
        if self._core is None:
            self._core = CoreV1Api(self._api_client)
        return self._core

    @property
    def _auth_v1(self) -> AuthorizationV1Api:
        """Lazily create and cache an :class:`AuthorizationV1Api`."""
        if self._auth is None:
            self._auth = AuthorizationV1Api(self._api_client)
        return self._auth

    @property
    def _version_api(self) -> VersionApi:
        """Lazily create and cache a :class:`VersionApi`."""
        if self._version is None:
            self._version = VersionApi(self._api_client)
        return self._version

    @property
    def _batch_v1(self) -> BatchV1Api:
        """Lazily create and cache a :class:`BatchV1Api`."""
        if self._batch is None:
            self._batch = BatchV1Api(self._api_client)
        return self._batch

    # ------------------------------------------------------------------
    # Connection helpers
    # ------------------------------------------------------------------

    def is_connected(self) -> bool:
        """Return ``True`` if the cluster is reachable."""
        try:
            self._version_api.get_code()
            return True
        except (ApiException, ConfigException, ClusterConnectionError):
            return False

    def get_server_version(self) -> str:
        """Return the Kubernetes server version string (e.g. ``"1.28.3"``)."""
        try:
            info = self._version_api.get_code()
            return f"{info.major}.{info.minor}".rstrip("+")
        except ApiException as exc:
            raise ClusterConnectionError(
                f"Failed to retrieve server version: {exc.reason}",
                suggestion="Check cluster connectivity with 'kubectl cluster-info'.",
                error_code="ILUM-010",
            ) from exc

    def get_current_context(self) -> str:
        """Return the name of the active kubeconfig context."""
        try:
            contexts, active = list_kube_config_contexts()
            return str(active["name"])
        except ConfigException as exc:
            raise ClusterConnectionError(
                f"Failed to read current context: {exc}",
                suggestion="Run 'kubectl config current-context' to verify your kubeconfig.",
                error_code="ILUM-010",
            ) from exc

    def list_contexts(self) -> list[str]:
        """Return the names of all contexts in the kubeconfig."""
        try:
            contexts, _ = list_kube_config_contexts()
            return [str(ctx["name"]) for ctx in contexts]
        except ConfigException as exc:
            raise ClusterConnectionError(
                f"Failed to list contexts: {exc}",
                suggestion="Ensure a valid kubeconfig is available.",
                error_code="ILUM-010",
            ) from exc

    # ------------------------------------------------------------------
    # Namespace
    # ------------------------------------------------------------------

    def namespace_exists(self, namespace: str) -> bool:
        """Return ``True`` if *namespace* exists on the cluster."""
        try:
            self._core_v1.read_namespace(name=namespace)
            return True
        except ApiException as exc:
            if exc.status == 404:
                return False
            raise ClusterConnectionError(
                f"Failed to check namespace '{namespace}': {exc.reason}",
                suggestion="Verify cluster connectivity and RBAC permissions.",
                error_code="ILUM-010",
            ) from exc

    def create_namespace(self, namespace: str) -> None:
        """Create *namespace* on the cluster.

        Raises :class:`ClusterConnectionError` when the API call fails
        (e.g. insufficient RBAC permissions).
        """
        from kubernetes.client import V1Namespace, V1ObjectMeta

        body = V1Namespace(metadata=V1ObjectMeta(name=namespace))
        try:
            self._core_v1.create_namespace(body=body)
        except ApiException as exc:
            raise ClusterConnectionError(
                f"Failed to create namespace '{namespace}': {exc.reason}",
                suggestion=(
                    f"Create the namespace manually with: kubectl create namespace {namespace}"
                ),
                error_code="ILUM-010",
            ) from exc

    # ------------------------------------------------------------------
    # Nodes & Service queries
    # ------------------------------------------------------------------

    def get_node_address(self) -> str:
        """Return the first node's IP address (prefers ExternalIP, falls back to InternalIP)."""
        try:
            nodes = self._core_v1.list_node()
        except ApiException as exc:
            raise ClusterConnectionError(
                f"Failed to list nodes: {exc.reason}",
                suggestion="Check cluster connectivity and RBAC permissions.",
                error_code="ILUM-010",
            ) from exc

        if not nodes.items:
            raise ClusterConnectionError(
                "No nodes found in the cluster.",
                suggestion="Ensure the cluster is running and accessible.",
                error_code="ILUM-010",
            )

        node = nodes.items[0]
        external_ip = ""
        internal_ip = ""
        for addr in node.status.addresses or []:
            if addr.type == "ExternalIP" and not external_ip:
                external_ip = addr.address
            elif addr.type == "InternalIP" and not internal_ip:
                internal_ip = addr.address
        result = external_ip or internal_ip
        if not result:
            raise ClusterConnectionError(
                "Could not determine node IP address.",
                suggestion="Check node status with 'kubectl get nodes -o wide'.",
                error_code="ILUM-010",
            )
        return result

    def get_service_node_port(self, service_name: str, namespace: str) -> int:
        """Return the nodePort for the first port of a given service."""
        try:
            svc = self._core_v1.read_namespaced_service(name=service_name, namespace=namespace)
        except ApiException as exc:
            if exc.status == 404:
                raise ClusterConnectionError(
                    f"Service '{service_name}' not found in namespace '{namespace}'.",
                    suggestion=(
                        f"Check the service exists: kubectl get svc {service_name} -n {namespace}"
                    ),
                    error_code="ILUM-010",
                ) from exc
            raise ClusterConnectionError(
                f"Failed to read service '{service_name}': {exc.reason}",
                suggestion="Check cluster connectivity and RBAC permissions.",
                error_code="ILUM-010",
            ) from exc

        for port in svc.spec.ports or []:
            if port.node_port:
                return int(port.node_port)

        raise ClusterConnectionError(
            f"Service '{service_name}' has no NodePort allocated.",
            suggestion=(
                f"Ensure the service type is NodePort: "
                f"kubectl get svc {service_name} -n {namespace}"
            ),
            error_code="ILUM-010",
        )

    # ------------------------------------------------------------------
    # NodePorts
    # ------------------------------------------------------------------

    @dataclass
    class NodePortInfo:
        """A single allocated NodePort on the cluster."""

        port: int
        service_name: str
        namespace: str

    def list_node_ports(self) -> list[KubeClient.NodePortInfo]:
        """Return all allocated NodePorts across every namespace."""
        try:
            svc_list = self._core_v1.list_service_for_all_namespaces()
        except ApiException as exc:
            raise ClusterConnectionError(
                f"Failed to list cluster services: {exc.reason}",
                suggestion="Check cluster connectivity and RBAC permissions.",
                error_code="ILUM-010",
            ) from exc

        result: list[KubeClient.NodePortInfo] = []
        for svc in svc_list.items:
            if svc.spec.type not in ("NodePort", "LoadBalancer"):
                continue
            for port in svc.spec.ports or []:
                if port.node_port:
                    result.append(
                        KubeClient.NodePortInfo(
                            port=port.node_port,
                            service_name=svc.metadata.name,
                            namespace=svc.metadata.namespace,
                        )
                    )
        return result

    # ------------------------------------------------------------------
    # Pods
    # ------------------------------------------------------------------

    @staticmethod
    def _pod_to_status(pod: V1Pod) -> PodStatus:
        """Extract a :class:`PodStatus` from a Kubernetes pod object."""
        phase = pod.status.phase or "Unknown"
        message = pod.status.message or ""

        container_statuses = pod.status.container_statuses or []
        ready = all(cs.ready for cs in container_statuses) if container_statuses else False
        restart_count = sum(cs.restart_count for cs in container_statuses)

        return PodStatus(
            name=pod.metadata.name,
            namespace=pod.metadata.namespace,
            phase=phase,
            ready=ready,
            restart_count=restart_count,
            message=message,
            containers=tuple(c.name for c in pod.spec.containers or []),
        )

    def list_pods(self, namespace: str) -> list[PodStatus]:
        """List all pods in *namespace* and return their summarised status."""
        try:
            pod_list = self._core_v1.list_namespaced_pod(namespace=namespace)
        except ApiException as exc:
            raise ClusterConnectionError(
                f"Failed to list pods in namespace '{namespace}': {exc.reason}",
                suggestion="Check namespace exists and you have 'list' permission on pods.",
                error_code="ILUM-010",
            ) from exc

        return [self._pod_to_status(pod) for pod in pod_list.items]

    def list_pods_by_label(self, namespace: str, label_selector: str) -> list[PodStatus]:
        """List pods in *namespace* matching *label_selector*."""
        try:
            pod_list = self._core_v1.list_namespaced_pod(
                namespace=namespace, label_selector=label_selector
            )
        except ApiException as exc:
            raise ClusterConnectionError(
                f"Failed to list pods in '{namespace}' "
                f"with selector '{label_selector}': {exc.reason}",
                suggestion="Check namespace exists and you have 'list' permission on pods.",
                error_code="ILUM-010",
            ) from exc

        return [self._pod_to_status(pod) for pod in pod_list.items]

    def get_pod_health(self, namespace: str) -> list[PodStatus]:
        """Return only unhealthy pods (not ready or phase != Running)."""
        return [pod for pod in self.list_pods(namespace) if not pod.ready or pod.phase != "Running"]

    def read_pod_log(
        self,
        namespace: str,
        pod_name: str,
        *,
        container: str | None = None,
        tail_lines: int | None = 100,
        previous: bool = False,
    ) -> str:
        """Read logs from a pod and return them as a string."""
        kwargs: dict[str, object] = {"name": pod_name, "namespace": namespace, "previous": previous}
        if container:
            kwargs["container"] = container
        if tail_lines is not None:
            kwargs["tail_lines"] = tail_lines
        try:
            return str(self._core_v1.read_namespaced_pod_log(**kwargs))
        except ApiException as exc:
            raise ClusterConnectionError(
                f"Failed to read logs for pod '{pod_name}': {exc.reason}",
                suggestion="Check that the pod exists and you have 'get' permission on pods/log.",
                error_code="ILUM-010",
            ) from exc

    def stream_pod_log(
        self,
        namespace: str,
        pod_name: str,
        *,
        container: str | None = None,
        tail_lines: int | None = 100,
        previous: bool = False,
    ) -> Iterator[str]:
        """Stream logs from a pod line by line."""
        kwargs: dict[str, object] = {
            "name": pod_name,
            "namespace": namespace,
            "follow": True,
            "_preload_content": False,
            "previous": previous,
        }
        if container:
            kwargs["container"] = container
        if tail_lines is not None:
            kwargs["tail_lines"] = tail_lines
        try:
            resp = self._core_v1.read_namespaced_pod_log(**kwargs)
        except ApiException as exc:
            raise ClusterConnectionError(
                f"Failed to stream logs for pod '{pod_name}': {exc.reason}",
                suggestion="Check that the pod exists and you have 'get' permission on pods/log.",
                error_code="ILUM-010",
            ) from exc
        try:
            for line in resp:
                if isinstance(line, bytes):
                    line = line.decode("utf-8", errors="replace")
                yield line.rstrip("\n\r")
        finally:
            resp.close()

    def delete_pods_by_label(self, namespace: str, label_selector: str) -> list[str]:
        """Delete pods matching *label_selector* in *namespace*.

        Kubernetes will recreate pods managed by a Deployment or
        StatefulSet, effectively performing a restart.

        Returns a list of deleted pod names.
        """
        try:
            pod_list = self._core_v1.list_namespaced_pod(
                namespace=namespace,
                label_selector=label_selector,
            )
        except ApiException as exc:
            raise ClusterConnectionError(
                f"Failed to list pods in '{namespace}' "
                f"with selector '{label_selector}': {exc.reason}",
                suggestion=("Check namespace exists and you have 'list' permission on pods."),
                error_code="ILUM-010",
            ) from exc

        deleted: list[str] = []
        for pod in pod_list.items:
            name = pod.metadata.name
            try:
                self._core_v1.delete_namespaced_pod(name=name, namespace=namespace)
                deleted.append(name)
            except ApiException as exc:
                raise ClusterConnectionError(
                    f"Failed to delete pod '{name}': {exc.reason}",
                    suggestion=("Check RBAC permissions for pod deletion."),
                    error_code="ILUM-010",
                ) from exc
        return deleted

    # ------------------------------------------------------------------
    # Jobs
    # ------------------------------------------------------------------

    def create_job(self, namespace: str, body: Any) -> Any:
        """Create a Job in *namespace*."""
        try:
            return self._batch_v1.create_namespaced_job(namespace=namespace, body=body)
        except ApiException as exc:
            raise ClusterConnectionError(
                f"Failed to create Job in '{namespace}': {exc.reason}",
                suggestion="Check RBAC permissions for Job creation.",
                error_code="ILUM-010",
            ) from exc

    def get_job(self, name: str, namespace: str) -> Any:
        """Read a Job by name."""
        try:
            return self._batch_v1.read_namespaced_job(name=name, namespace=namespace)
        except ApiException as exc:
            raise ClusterConnectionError(
                f"Failed to read Job '{name}': {exc.reason}",
                suggestion="Check that the Job exists and you have 'get' permission.",
                error_code="ILUM-010",
            ) from exc

    def list_jobs_by_label(self, namespace: str, label_selector: str) -> list[Any]:
        """List Jobs in *namespace* matching *label_selector*."""
        try:
            result = self._batch_v1.list_namespaced_job(
                namespace=namespace, label_selector=label_selector
            )
            return list(result.items)
        except ApiException as exc:
            raise ClusterConnectionError(
                f"Failed to list Jobs in '{namespace}' "
                f"with selector '{label_selector}': {exc.reason}",
                suggestion="Check RBAC permissions for Job listing.",
                error_code="ILUM-010",
            ) from exc

    def get_job_pod_logs(self, job_name: str, namespace: str, tail_lines: int = 200) -> str:
        """Fetch logs from the pod spawned by a Job."""
        pods = self.list_pods_by_label(namespace, f"job-name={job_name}")
        if not pods:
            return ""
        return self.read_pod_log(namespace, pods[0].name, tail_lines=tail_lines)

    def delete_job(self, name: str, namespace: str) -> None:
        """Delete a Job with background propagation (also deletes its pods)."""
        from kubernetes.client import V1DeleteOptions

        try:
            self._batch_v1.delete_namespaced_job(
                name=name,
                namespace=namespace,
                body=V1DeleteOptions(propagation_policy="Background"),
            )
        except ApiException as exc:
            raise ClusterConnectionError(
                f"Failed to delete Job '{name}': {exc.reason}",
                suggestion="Check RBAC permissions for Job deletion.",
                error_code="ILUM-010",
            ) from exc

    # ------------------------------------------------------------------
    # PVCs
    # ------------------------------------------------------------------

    def list_pvcs(self, namespace: str) -> list[PVCStatus]:
        """List PersistentVolumeClaims in *namespace*."""
        try:
            pvc_list = self._core_v1.list_namespaced_persistent_volume_claim(namespace=namespace)
        except ApiException as exc:
            raise ClusterConnectionError(
                f"Failed to list PVCs in namespace '{namespace}': {exc.reason}",
                suggestion="Check namespace exists and you have 'list' permission on PVCs.",
                error_code="ILUM-010",
            ) from exc

        result: list[PVCStatus] = []
        for pvc in pvc_list.items:
            phase = pvc.status.phase or "Unknown"
            storage_class = pvc.spec.storage_class_name or ""
            capacity = ""
            if pvc.status.capacity:
                capacity = pvc.status.capacity.get("storage", "")

            result.append(
                PVCStatus(
                    name=pvc.metadata.name,
                    namespace=pvc.metadata.namespace,
                    phase=phase,
                    storage_class=storage_class,
                    capacity=capacity,
                )
            )
        return result

    def list_storage_classes(self) -> list[dict[str, Any]]:
        """List StorageClasses with their default status."""
        try:
            from kubernetes.client import StorageV1Api

            storage_api = StorageV1Api(api_client=self._api_client)
            sc_list = storage_api.list_storage_class()
            result = []
            for sc in sc_list.items:
                annotations = sc.metadata.annotations or {}
                is_default = (
                    annotations.get("storageclass.kubernetes.io/is-default-class", "").lower()
                    == "true"
                    or annotations.get(
                        "storageclass.beta.kubernetes.io/is-default-class", ""
                    ).lower()
                    == "true"
                )
                result.append(
                    {
                        "name": sc.metadata.name,
                        "provisioner": sc.provisioner,
                        "is_default": is_default,
                    }
                )
            return result
        except Exception as exc:
            raise ClusterConnectionError(
                f"Failed to list StorageClasses: {exc}",
                suggestion="Check cluster connectivity.",
                error_code="ILUM-010",
            ) from exc

    def delete_pvcs(self, namespace: str) -> list[str]:
        """Delete all PersistentVolumeClaims in *namespace*.

        Returns a list of deleted PVC names.
        """
        try:
            pvc_list = self._core_v1.list_namespaced_persistent_volume_claim(namespace=namespace)
        except ApiException as exc:
            raise ClusterConnectionError(
                f"Failed to list PVCs in namespace '{namespace}': {exc.reason}",
                suggestion="Check namespace exists and you have 'list' permission on PVCs.",
                error_code="ILUM-010",
            ) from exc

        deleted: list[str] = []
        for pvc in pvc_list.items:
            name = pvc.metadata.name
            try:
                self._core_v1.delete_namespaced_persistent_volume_claim(
                    name=name, namespace=namespace
                )
                deleted.append(name)
            except ApiException as exc:
                raise ClusterConnectionError(
                    f"Failed to delete PVC '{name}': {exc.reason}",
                    suggestion="Check RBAC permissions for PVC deletion.",
                    error_code="ILUM-010",
                ) from exc
        return deleted

    def delete_namespace(self, namespace: str) -> None:
        """Delete *namespace* from the cluster."""
        try:
            self._core_v1.delete_namespace(name=namespace)
        except ApiException as exc:
            raise ClusterConnectionError(
                f"Failed to delete namespace '{namespace}': {exc.reason}",
                suggestion="Check RBAC permissions for namespace deletion.",
                error_code="ILUM-010",
            ) from exc

    # ------------------------------------------------------------------
    # Metrics
    # ------------------------------------------------------------------

    def metrics_available(self) -> bool:
        """Return ``True`` if the metrics.k8s.io API group is available."""
        try:
            from kubernetes.client import CustomObjectsApi

            api = CustomObjectsApi(self._api_client)
            api.list_cluster_custom_object(
                group="metrics.k8s.io",
                version="v1beta1",
                plural="nodes",
            )
            return True
        except ApiException:
            return False
        except Exception:
            return False

    def list_pod_metrics(self, namespace: str) -> list[PodMetrics]:
        """Fetch pod-level CPU and memory usage from the Metrics API."""
        try:
            from kubernetes.client import CustomObjectsApi

            api = CustomObjectsApi(self._api_client)
            result = api.list_namespaced_custom_object(
                group="metrics.k8s.io",
                version="v1beta1",
                namespace=namespace,
                plural="pods",
            )
        except ApiException as exc:
            raise ClusterConnectionError(
                f"Failed to fetch pod metrics: {exc.reason}",
                suggestion="Ensure metrics-server is installed: kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml",
                error_code="ILUM-072",
            ) from exc

        metrics: list[PodMetrics] = []
        for item in result.get("items", []):
            name = item["metadata"]["name"]
            ns = item["metadata"]["namespace"]
            total_cpu = 0
            total_mem = 0
            for container in item.get("containers", []):
                usage = container.get("usage", {})
                total_cpu += self._parse_cpu(usage.get("cpu", "0"))
                total_mem += self._parse_memory(usage.get("memory", "0"))
            metrics.append(
                PodMetrics(
                    name=name, namespace=ns, cpu_millicores=total_cpu, memory_bytes=total_mem
                )
            )
        return metrics

    def list_pod_resources(self, namespace: str) -> list[PodResources]:
        """Extract resource requests and limits from pod specs."""
        try:
            pod_list = self._core_v1.list_namespaced_pod(namespace=namespace)
        except ApiException as exc:
            raise ClusterConnectionError(
                f"Failed to list pods for resource info: {exc.reason}",
                error_code="ILUM-010",
            ) from exc

        result: list[PodResources] = []
        for pod in pod_list.items:
            if pod.status.phase in ("Succeeded", "Failed"):
                continue
            cpu_req = 0
            cpu_lim = 0
            mem_req = 0
            mem_lim = 0
            for c in pod.spec.containers or []:
                res = c.resources
                if res:
                    if res.requests:
                        cpu_req += self._parse_cpu(res.requests.get("cpu", "0"))
                        mem_req += self._parse_memory(res.requests.get("memory", "0"))
                    if res.limits:
                        cpu_lim += self._parse_cpu(res.limits.get("cpu", "0"))
                        mem_lim += self._parse_memory(res.limits.get("memory", "0"))
            result.append(
                PodResources(
                    name=pod.metadata.name,
                    cpu_request_millicores=cpu_req,
                    cpu_limit_millicores=cpu_lim,
                    memory_request_bytes=mem_req,
                    memory_limit_bytes=mem_lim,
                )
            )
        return result

    # ------------------------------------------------------------------
    # Service health
    # ------------------------------------------------------------------

    def list_service_endpoints(self, namespace: str, service_name: str) -> bool:
        """Return ``True`` if *service_name* in *namespace* has at least 1 ready endpoint."""
        try:
            endpoints = self._core_v1.read_namespaced_endpoints(
                name=service_name, namespace=namespace
            )
            return any(subset.addresses for subset in endpoints.subsets or [])
        except ApiException as exc:
            if exc.status == 404:
                return False
            raise ClusterConnectionError(
                f"Failed to check endpoints for '{service_name}': {exc.reason}",
                suggestion="Check namespace and service name.",
                error_code="ILUM-010",
            ) from exc

    def check_health_endpoint(
        self,
        namespace: str,
        pod_name: str,
        port: int,
        path: str,
        timeout: int = 5,
    ) -> tuple[bool, str]:
        """Hit a health endpoint via pod port-forward proxy.

        Returns ``(healthy, message)`` — *healthy* is True when the
        endpoint returns 2xx.
        """
        try:
            resp = self._core_v1.connect_get_namespaced_pod_proxy_with_path(
                name=f"{pod_name}:{port}",
                namespace=namespace,
                path=path.lstrip("/"),
                _request_timeout=timeout,
            )
            return True, str(resp)[:200]
        except ApiException as exc:
            return False, f"HTTP {exc.status}: {exc.reason}"
        except Exception as exc:
            return False, str(exc)[:200]

    # ------------------------------------------------------------------
    # Events
    # ------------------------------------------------------------------

    def list_events(self, namespace: str) -> list[EventInfo]:
        """List events in *namespace*."""
        try:
            event_list = self._core_v1.list_namespaced_event(namespace=namespace)
        except ApiException as exc:
            raise ClusterConnectionError(
                f"Failed to list events in namespace '{namespace}': {exc.reason}",
                suggestion="Check namespace exists and you have 'list' permission on events.",
                error_code="ILUM-071",
            ) from exc

        result: list[EventInfo] = []
        for ev in event_list.items:
            obj = ev.involved_object
            involved = f"{obj.kind}/{obj.name}" if obj else ""
            source_comp = ""
            if ev.source and ev.source.component:
                source_comp = ev.source.component

            result.append(
                EventInfo(
                    name=ev.metadata.name or "",
                    namespace=ev.metadata.namespace or namespace,
                    reason=ev.reason or "",
                    message=ev.message or "",
                    event_type=ev.type or "Normal",
                    involved_object=involved,
                    count=ev.count or 1,
                    first_seen=ev.first_timestamp.isoformat() if ev.first_timestamp else "",
                    last_seen=ev.last_timestamp.isoformat() if ev.last_timestamp else "",
                    source=source_comp,
                )
            )
        return result

    # ------------------------------------------------------------------
    # RBAC
    # ------------------------------------------------------------------

    def get_cluster_resources(self) -> dict[str, int]:
        """Get aggregate allocatable resources across all nodes.

        Returns a dict with 'allocatable_memory_bytes', 'allocatable_cpu_millicores', 'node_count'.
        """
        try:
            nodes = self._core_v1.list_node()
            total_memory = 0
            total_cpu = 0
            for node in nodes.items:
                alloc = node.status.allocatable or {}
                # Memory is like "16384Mi" or "16Gi" or "17179869184" (bytes)
                mem_str = alloc.get("memory", "0")
                total_memory += self._parse_memory(mem_str)
                # CPU is like "4" or "4000m"
                cpu_str = alloc.get("cpu", "0")
                total_cpu += self._parse_cpu(cpu_str)
            return {
                "allocatable_memory_bytes": total_memory,
                "allocatable_cpu_millicores": total_cpu,
                "node_count": len(nodes.items),
            }
        except Exception as exc:
            raise ClusterConnectionError(
                f"Failed to get cluster resources: {exc}",
                suggestion="Check cluster connectivity.",
                error_code="ILUM-010",
            ) from exc

    @staticmethod
    def _parse_memory(value: str) -> int:
        """Parse Kubernetes memory value to bytes."""
        value = str(value).strip()
        if value.endswith("Ki"):
            return int(value[:-2]) * 1024
        elif value.endswith("Mi"):
            return int(value[:-2]) * 1024 * 1024
        elif value.endswith("Gi"):
            return int(value[:-2]) * 1024 * 1024 * 1024
        elif value.endswith("Ti"):
            return int(value[:-2]) * 1024 * 1024 * 1024 * 1024
        elif value.endswith("m"):
            # millibytes (rare but valid)
            return int(value[:-1]) // 1000
        else:
            return int(value)

    @staticmethod
    def _parse_cpu(value: str) -> int:
        """Parse Kubernetes CPU value to millicores."""
        value = str(value).strip()
        if value.endswith("n"):
            return int(value[:-1]) // 1_000_000
        elif value.endswith("u"):
            return int(value[:-1]) // 1_000
        elif value.endswith("m"):
            return int(value[:-1])
        else:
            return int(float(value) * 1000)

    # ------------------------------------------------------------------
    # RBAC
    # ------------------------------------------------------------------

    def check_rbac(
        self,
        namespace: str,
        verbs: list[str] | None = None,
        resources: list[str] | None = None,
    ) -> bool:
        """Check whether the current user has the requested permissions.

        Uses ``SelfSubjectAccessReview`` to verify each *verb*/*resource*
        combination. Returns ``True`` only if **all** permissions are granted.
        """
        verbs = verbs or _DEFAULT_VERBS
        resources = resources or _DEFAULT_RESOURCES

        try:
            for resource in resources:
                for verb in verbs:
                    review = V1SelfSubjectAccessReview(
                        spec=V1SelfSubjectAccessReviewSpec(
                            resource_attributes=V1ResourceAttributes(
                                namespace=namespace,
                                verb=verb,
                                resource=resource,
                            )
                        )
                    )
                    response = self._auth_v1.create_self_subject_access_review(body=review)
                    if not response.status.allowed:
                        return False
        except ApiException as exc:
            raise ClusterConnectionError(
                f"Failed to check RBAC permissions: {exc.reason}",
                suggestion="Ensure the cluster supports SelfSubjectAccessReview.",
                error_code="ILUM-010",
            ) from exc

        return True
