"""Real K8s client — wraps the kubernetes Python library.

Implements K8sClientProtocol. Handles retry, dict normalization,
connection pooling, and token refresh. SDK services never import
this directly — they depend on K8sClientProtocol.
"""

from __future__ import annotations

import base64
import time
from collections.abc import Iterator
from typing import Any

import kubernetes  # type: ignore[import-untyped]
from kubernetes import watch
from kubernetes.client.exceptions import ApiException  # type: ignore[import-untyped]
from kubernetes.stream import stream  # type: ignore[import-untyped]

_MAX_RETRIES = 5
_RETRY_BACKOFF = 0.5  # seconds, doubled each retry
_RETRYABLE_STATUSES = {429, 503}


def _to_dict(obj: Any) -> dict[str, Any]:
    """Convert a K8s API model object to a plain dict (snake_case keys).

    Kept as module-level for unit tests that mock K8s API responses.
    Production code uses K8sClient._serialize() for camelCase output.
    """
    if isinstance(obj, dict):
        return obj
    if hasattr(obj, "to_dict"):
        return obj.to_dict()  # type: ignore[no-any-return]
    raise TypeError(f"Cannot convert {type(obj)} to dict")


def _label_selector_str(labels: dict[str, str]) -> str:
    """Convert a label dict to a K8s label selector string."""
    return ",".join(f"{k}={v}" for k, v in labels.items())


class K8sClient:
    """Real K8s client. Implements K8sClientProtocol."""

    def __init__(self, api_client: kubernetes.client.ApiClient) -> None:
        self._api_client = api_client
        self._core = kubernetes.client.CoreV1Api(api_client)
        self._apps = kubernetes.client.AppsV1Api(api_client)
        self._networking = kubernetes.client.NetworkingV1Api(api_client)
        self._batch = kubernetes.client.BatchV1Api(api_client)
        self._autoscaling = kubernetes.client.AutoscalingV2Api(api_client)
        self._auth = kubernetes.client.AuthorizationV1Api(api_client)

    @classmethod
    def from_context(cls, context: str | None = None) -> K8sClient:
        """Create client from kubeconfig context."""
        config = kubernetes.client.Configuration()
        kubernetes.config.load_kube_config(context=context, client_configuration=config)
        api_client = kubernetes.client.ApiClient(config)
        return cls(api_client)

    @property
    def cluster_endpoint(self) -> str:
        """Get the cluster API server endpoint from the client configuration."""
        try:
            host: str = self._api_client.configuration.host or ""
            return host
        except Exception:
            return ""

    def _serialize(self, obj: Any) -> dict[str, Any]:
        """Convert a K8s API model object to a plain dict with camelCase keys.

        Uses api_client.sanitize_for_serialization() which produces the
        same key format as the K8s JSON API (camelCase), matching FakeK8sClient.
        Falls back to _to_dict() for plain dicts and non-model objects.
        """
        if isinstance(obj, dict):
            return obj
        result: Any = self._api_client.sanitize_for_serialization(obj)
        if isinstance(result, dict):
            return result
        return _to_dict(obj)

    def _retry(self, fn: Any, *args: Any, **kwargs: Any) -> Any:
        """Retry transient errors with exponential backoff."""
        backoff = _RETRY_BACKOFF
        for attempt in range(_MAX_RETRIES):
            try:
                return fn(*args, **kwargs)
            except ApiException as e:
                if e.status in _RETRYABLE_STATUSES and attempt < _MAX_RETRIES - 1:
                    time.sleep(backoff)
                    backoff *= 2
                    continue
                raise

    # ── Namespaces ──────────────────────────────────────────────

    def get_namespace(self, name: str) -> dict[str, Any]:
        return self._serialize(self._retry(self._core.read_namespace, name))

    def namespace_exists(self, name: str) -> bool:
        try:
            self._core.read_namespace(name)
            return True
        except ApiException as e:
            if e.status == 404:
                return False
            raise

    def create_namespace(self, name: str) -> dict[str, Any]:
        body = kubernetes.client.V1Namespace(metadata=kubernetes.client.V1ObjectMeta(name=name))
        return self._serialize(self._retry(self._core.create_namespace, body))

    # ── Deployments ─────────────────────────────────────────────

    def create_deployment(self, namespace: str, body: dict[str, Any]) -> dict[str, Any]:
        return self._serialize(
            self._retry(self._apps.create_namespaced_deployment, namespace, body)
        )

    def get_deployment(self, namespace: str, name: str) -> dict[str, Any]:
        return self._serialize(self._retry(self._apps.read_namespaced_deployment, name, namespace))

    def update_deployment(self, namespace: str, name: str, body: dict[str, Any]) -> dict[str, Any]:
        return self._serialize(
            self._retry(self._apps.replace_namespaced_deployment, name, namespace, body)
        )

    def delete_deployment(self, namespace: str, name: str) -> None:
        self._retry(self._apps.delete_namespaced_deployment, name, namespace)

    def list_deployments(
        self, namespace: str, labels: dict[str, str] | None = None
    ) -> list[dict[str, Any]]:
        kwargs: dict[str, Any] = {}
        if labels:
            kwargs["label_selector"] = _label_selector_str(labels)
        result = self._retry(self._apps.list_namespaced_deployment, namespace, **kwargs)
        return [self._serialize(item) for item in result.items]

    # ── Pods ────────────────────────────────────────────────────

    def create_pod(self, namespace: str, body: dict[str, Any]) -> dict[str, Any]:
        return self._serialize(self._retry(self._core.create_namespaced_pod, namespace, body))

    def list_pods(
        self, namespace: str, labels: dict[str, str] | None = None
    ) -> list[dict[str, Any]]:
        kwargs: dict[str, Any] = {}
        if labels:
            kwargs["label_selector"] = _label_selector_str(labels)
        result = self._retry(self._core.list_namespaced_pod, namespace, **kwargs)
        return [self._serialize(item) for item in result.items]

    def delete_pod(self, namespace: str, name: str) -> None:
        self._retry(self._core.delete_namespaced_pod, name, namespace)

    def exec_in_pod(
        self,
        namespace: str,
        name: str,
        command: list[str],
        tty: bool = False,
        container: str | None = None,
        stdin_data: str | None = None,
    ) -> str:
        needs_stdin = tty or stdin_data is not None
        kwargs: dict[str, Any] = {
            "command": command,
            "stdin": needs_stdin,
            "stdout": True,
            "stderr": True,
            "tty": tty,
            "_preload_content": False,
        }
        if container is not None:
            kwargs["container"] = container

        client = stream(
            self._core.connect_get_namespaced_pod_exec,
            name,
            namespace,
            **kwargs,
        )
        if stdin_data is not None:
            client.write_stdin(stdin_data)
            # Signal EOF by sending EOT (Ctrl-D) on stdin channel.
            # write_stdin("") does NOT close the stdin stream — the remote
            # process (e.g. psql) hangs forever waiting for more input.
            client.write_channel(0, "\x04")
        timeout = 120 if stdin_data is not None else 60
        client.run_forever(timeout=timeout)
        # read_stdout/read_stderr default to timeout=None (infinite wait).
        # If the websocket is in a broken state after run_forever, the read
        # blocks forever.  Use an explicit timeout to prevent hangs.
        stdout = client.read_stdout(timeout=10)
        stderr = client.read_stderr(timeout=10)
        rc = client.returncode
        client.close()
        if rc != 0:
            raise RuntimeError(f"Command exited with code {rc}: {stderr or stdout}")
        return str(stdout)

    def stream_pod_logs(
        self,
        namespace: str,
        name: str,
        follow: bool = False,
        tail_lines: int | None = None,
        since_seconds: int | None = None,
        previous: bool = False,
    ) -> Iterator[str]:
        kwargs: dict[str, Any] = {
            "follow": follow,
            "previous": previous,
            "_preload_content": False,
        }
        if tail_lines is not None:
            kwargs["tail_lines"] = tail_lines
        if since_seconds is not None:
            kwargs["since_seconds"] = since_seconds
        resp = self._core.read_namespaced_pod_log(name, namespace, **kwargs)
        for line in resp:
            yield line.decode("utf-8").rstrip("\n")

    # ── ConfigMaps ──────────────────────────────────────────────

    def create_configmap(self, namespace: str, body: dict[str, Any]) -> dict[str, Any]:
        return self._serialize(
            self._retry(self._core.create_namespaced_config_map, namespace, body)
        )

    def get_configmap(self, namespace: str, name: str) -> dict[str, Any]:
        return self._serialize(self._retry(self._core.read_namespaced_config_map, name, namespace))

    def update_configmap(self, namespace: str, name: str, body: dict[str, Any]) -> dict[str, Any]:
        return self._serialize(
            self._retry(self._core.replace_namespaced_config_map, name, namespace, body)
        )

    def delete_configmap(self, namespace: str, name: str) -> None:
        self._retry(self._core.delete_namespaced_config_map, name, namespace)

    def list_configmaps(
        self, namespace: str, labels: dict[str, str] | None = None
    ) -> list[dict[str, Any]]:
        kwargs: dict[str, Any] = {}
        if labels:
            kwargs["label_selector"] = _label_selector_str(labels)
        result = self._retry(self._core.list_namespaced_config_map, namespace, **kwargs)
        return [self._serialize(item) for item in result.items]

    # ── Secrets ─────────────────────────────────────────────────

    def create_secret(self, namespace: str, body: dict[str, Any]) -> dict[str, Any]:
        return self._serialize(self._retry(self._core.create_namespaced_secret, namespace, body))

    def get_secret(self, namespace: str, name: str) -> dict[str, Any]:
        result = self._serialize(self._retry(self._core.read_namespaced_secret, name, namespace))
        # Real K8s returns base64-encoded values in 'data'. Decode them so
        # SDK code always sees plain-text strings (matching FakeK8sClient).
        data = result.get("data")
        if data and isinstance(data, dict):
            result["data"] = {
                k: base64.b64decode(v).decode("utf-8") if v else "" for k, v in data.items()
            }
        return result

    def update_secret(self, namespace: str, name: str, body: dict[str, Any]) -> dict[str, Any]:
        return self._serialize(
            self._retry(self._core.replace_namespaced_secret, name, namespace, body)
        )

    def delete_secret(self, namespace: str, name: str) -> None:
        self._retry(self._core.delete_namespaced_secret, name, namespace)

    def list_secrets(
        self, namespace: str, labels: dict[str, str] | None = None
    ) -> list[dict[str, Any]]:
        kwargs: dict[str, Any] = {}
        if labels:
            kwargs["label_selector"] = _label_selector_str(labels)
        result = self._retry(self._core.list_namespaced_secret, namespace, **kwargs)
        items: list[dict[str, Any]] = []
        for item in result.items:
            serialized = self._serialize(item)
            # Decode base64 data values (matching get_secret behavior)
            data = serialized.get("data")
            if data and isinstance(data, dict):
                serialized["data"] = {
                    k: base64.b64decode(v).decode("utf-8") if v else "" for k, v in data.items()
                }
            items.append(serialized)
        return items

    # ── Services ────────────────────────────────────────────────

    def create_service(self, namespace: str, body: dict[str, Any]) -> dict[str, Any]:
        return self._serialize(self._retry(self._core.create_namespaced_service, namespace, body))

    def get_service(self, namespace: str, name: str) -> dict[str, Any]:
        return self._serialize(self._retry(self._core.read_namespaced_service, name, namespace))

    def update_service(self, namespace: str, name: str, body: dict[str, Any]) -> dict[str, Any]:
        return self._serialize(
            self._retry(self._core.replace_namespaced_service, name, namespace, body)
        )

    def delete_service(self, namespace: str, name: str) -> None:
        self._retry(self._core.delete_namespaced_service, name, namespace)

    def list_services(
        self, namespace: str, labels: dict[str, str] | None = None
    ) -> list[dict[str, Any]]:
        kwargs: dict[str, Any] = {}
        if labels:
            kwargs["label_selector"] = _label_selector_str(labels)
        result = self._retry(self._core.list_namespaced_service, namespace, **kwargs)
        return [self._serialize(item) for item in result.items]

    # ── Ingress ─────────────────────────────────────────────────

    def create_ingress(self, namespace: str, body: dict[str, Any]) -> dict[str, Any]:
        return self._serialize(
            self._retry(self._networking.create_namespaced_ingress, namespace, body)
        )

    def get_ingress(self, namespace: str, name: str) -> dict[str, Any]:
        return self._serialize(
            self._retry(self._networking.read_namespaced_ingress, name, namespace)
        )

    def update_ingress(self, namespace: str, name: str, body: dict[str, Any]) -> dict[str, Any]:
        return self._serialize(
            self._retry(self._networking.replace_namespaced_ingress, name, namespace, body)
        )

    def delete_ingress(self, namespace: str, name: str) -> None:
        self._retry(self._networking.delete_namespaced_ingress, name, namespace)

    def list_ingresses(
        self, namespace: str, labels: dict[str, str] | None = None
    ) -> list[dict[str, Any]]:
        kwargs: dict[str, Any] = {}
        if labels:
            kwargs["label_selector"] = _label_selector_str(labels)
        result = self._retry(self._networking.list_namespaced_ingress, namespace, **kwargs)
        return [self._serialize(item) for item in result.items]

    def list_ingress_classes(self) -> list[dict[str, Any]]:
        result = self._retry(self._networking.list_ingress_class)
        return [self._serialize(item) for item in result.items]

    def create_ingress_class(self, body: dict[str, Any]) -> None:
        self._retry(self._networking.create_ingress_class, body)

    # ── StatefulSets ────────────────────────────────────────────

    def create_statefulset(self, namespace: str, body: dict[str, Any]) -> dict[str, Any]:
        return self._serialize(
            self._retry(self._apps.create_namespaced_stateful_set, namespace, body)
        )

    def get_statefulset(self, namespace: str, name: str) -> dict[str, Any]:
        return self._serialize(
            self._retry(self._apps.read_namespaced_stateful_set, name, namespace)
        )

    def update_statefulset(
        self, namespace: str, name: str, body: dict[str, Any]
    ) -> dict[str, Any]:
        return self._serialize(
            self._retry(
                self._apps.replace_namespaced_stateful_set,
                name,
                namespace,
                body,
            )
        )

    def delete_statefulset(self, namespace: str, name: str) -> None:
        self._retry(self._apps.delete_namespaced_stateful_set, name, namespace)

    def list_statefulsets(
        self, namespace: str, labels: dict[str, str] | None = None
    ) -> list[dict[str, Any]]:
        kwargs: dict[str, Any] = {}
        if labels:
            kwargs["label_selector"] = _label_selector_str(labels)
        result = self._retry(self._apps.list_namespaced_stateful_set, namespace, **kwargs)
        return [self._serialize(item) for item in result.items]

    # ── PVCs ────────────────────────────────────────────────────

    def create_pvc(self, namespace: str, body: dict[str, Any]) -> dict[str, Any]:
        return self._serialize(
            self._retry(
                self._core.create_namespaced_persistent_volume_claim,
                namespace,
                body,
            )
        )

    def get_pvc(self, namespace: str, name: str) -> dict[str, Any]:
        return self._serialize(
            self._retry(
                self._core.read_namespaced_persistent_volume_claim,
                name,
                namespace,
            )
        )

    def delete_pvc(self, namespace: str, name: str) -> None:
        self._retry(
            self._core.delete_namespaced_persistent_volume_claim,
            name,
            namespace,
        )

    def list_pvcs(
        self, namespace: str, labels: dict[str, str] | None = None
    ) -> list[dict[str, Any]]:
        kwargs: dict[str, Any] = {}
        if labels:
            kwargs["label_selector"] = _label_selector_str(labels)
        result = self._retry(
            self._core.list_namespaced_persistent_volume_claim, namespace, **kwargs
        )
        return [self._serialize(item) for item in result.items]

    # ── Jobs ────────────────────────────────────────────────────

    def create_job(self, namespace: str, body: dict[str, Any]) -> dict[str, Any]:
        return self._serialize(self._retry(self._batch.create_namespaced_job, namespace, body))

    def wait_for_job(self, namespace: str, name: str, timeout: int = 300) -> dict[str, Any]:
        w = watch.Watch()
        for event in w.stream(
            self._batch.list_namespaced_job,
            namespace,
            field_selector=f"metadata.name={name}",
            timeout_seconds=timeout,
        ):
            job = event["object"]
            if hasattr(job, "status"):
                if getattr(job.status, "succeeded", None):
                    w.stop()
                    return self._serialize(job)
                if getattr(job.status, "failed", None):
                    w.stop()
                    return self._serialize(job)
        return self._serialize(self._retry(self._batch.read_namespaced_job, name, namespace))

    def delete_job(self, namespace: str, name: str) -> None:
        self._retry(
            self._batch.delete_namespaced_job,
            name,
            namespace,
            propagation_policy="Background",
        )

    # ── HPA ─────────────────────────────────────────────────────

    def create_hpa(self, namespace: str, body: dict[str, Any]) -> dict[str, Any]:
        return self._serialize(
            self._retry(
                self._autoscaling.create_namespaced_horizontal_pod_autoscaler,
                namespace,
                body,
            )
        )

    def get_hpa(self, namespace: str, name: str) -> dict[str, Any]:
        return self._serialize(
            self._retry(
                self._autoscaling.read_namespaced_horizontal_pod_autoscaler,
                name,
                namespace,
            )
        )

    def update_hpa(self, namespace: str, name: str, body: dict[str, Any]) -> dict[str, Any]:
        return self._serialize(
            self._retry(
                self._autoscaling.replace_namespaced_horizontal_pod_autoscaler,
                name,
                namespace,
                body,
            )
        )

    def delete_hpa(self, namespace: str, name: str) -> None:
        self._retry(
            self._autoscaling.delete_namespaced_horizontal_pod_autoscaler,
            name,
            namespace,
        )

    # ── NetworkPolicies ────────────────────────────────────────

    def create_network_policy(self, namespace: str, body: dict[str, Any]) -> dict[str, Any]:
        return self._serialize(
            self._retry(self._networking.create_namespaced_network_policy, namespace, body)
        )

    def get_network_policy(self, namespace: str, name: str) -> dict[str, Any]:
        return self._serialize(
            self._retry(self._networking.read_namespaced_network_policy, name, namespace)
        )

    def update_network_policy(
        self, namespace: str, name: str, body: dict[str, Any]
    ) -> dict[str, Any]:
        return self._serialize(
            self._retry(
                self._networking.replace_namespaced_network_policy,
                name,
                namespace,
                body,
            )
        )

    def delete_network_policy(self, namespace: str, name: str) -> None:
        self._retry(self._networking.delete_namespaced_network_policy, name, namespace)

    def list_network_policies(
        self, namespace: str, labels: dict[str, str] | None = None
    ) -> list[dict[str, Any]]:
        kwargs: dict[str, Any] = {}
        if labels:
            kwargs["label_selector"] = _label_selector_str(labels)
        result = self._retry(self._networking.list_namespaced_network_policy, namespace, **kwargs)
        return [self._serialize(item) for item in result.items]

    # ── DaemonSets ────────────────────────────────────────────────

    def list_daemonsets(
        self, namespace: str, labels: dict[str, str] | None = None
    ) -> list[dict[str, Any]]:
        kwargs: dict[str, Any] = {}
        if labels:
            kwargs["label_selector"] = _label_selector_str(labels)
        result = self._retry(self._apps.list_namespaced_daemon_set, namespace, **kwargs)
        return [self._serialize(item) for item in result.items]

    # ── Nodes ────────────────────────────────────────────────────

    def list_nodes(self) -> list[dict[str, Any]]:
        result = self._retry(self._core.list_node)
        return [self._serialize(item) for item in result.items]

    # ── Server version ───────────────────────────────────────────

    def server_version(self) -> str:
        self._version_api = getattr(self, "_version_api", None) or kubernetes.client.VersionApi(
            self._api_client
        )
        info = self._retry(self._version_api.get_code)
        return f"{info.major}.{info.minor}"

    # ── Auth ────────────────────────────────────────────────────

    def can_i(
        self,
        verb: str,
        resource: str,
        namespace: str = "",
        api_group: str = "",
        subresource: str = "",
    ) -> bool:
        attrs: dict[str, Any] = {
            "namespace": namespace,
            "verb": verb,
            "resource": resource,
        }
        if api_group:
            attrs["group"] = api_group
        if subresource:
            attrs["subresource"] = subresource
        review = kubernetes.client.V1SelfSubjectAccessReview(
            spec=kubernetes.client.V1SelfSubjectAccessReviewSpec(
                resource_attributes=kubernetes.client.V1ResourceAttributes(**attrs)
            )
        )
        result = self._auth.create_self_subject_access_review(review)
        return bool(result.status.allowed)

    # ── StorageClasses ────────────────────────────────────────

    def list_storage_classes(self) -> list[dict[str, Any]]:
        self._storage = getattr(self, "_storage", None) or kubernetes.client.StorageV1Api(
            self._api_client
        )
        result = self._retry(self._storage.list_storage_class)
        return [self._serialize(item) for item in result.items]
