"""Check registry — single source of truth for cluster requirements.

Both ``clusters:doctor`` and ``clusters:setup`` iterate the same
``CLUSTER_REQUIREMENTS`` tuple. Each requirement has a check function
and an optional fix function.
"""

from __future__ import annotations

import contextlib
import subprocess
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any

from kuberoku.models import CheckResult, FixResult
from kuberoku.sdk.ingress import detect_cert_manager, detect_ingress_controller, detect_lb_support
from kuberoku.sdk.rbac import check_rbac

if TYPE_CHECKING:
    from kuberoku.factory import KuberokuFactory
    from kuberoku.k8s.protocols import K8sClientProtocol

# Type aliases for check/fix callables
CheckFn = Any  # Callable[[K8sClientProtocol, str, str, str], CheckResult]
FixFn = Any  # Callable[[K8sClientProtocol, str, str, str], FixResult] | None


@dataclass(frozen=True)
class ClusterRequirement:
    """A single cluster requirement with check and optional fix."""

    name: str
    category: str
    description: str
    check_fn: CheckFn
    fix_fn: FixFn


# ── Check implementations ───────────────────────────────────────


def _check_api_reachable(k8s: K8sClientProtocol, ns: str, prefix: str, domain: str) -> CheckResult:
    """Check that the K8s API is reachable."""
    try:
        k8s.list_configmaps(ns, labels={})
        return CheckResult(
            name="api_reachable",
            category="connectivity",
            passed=True,
            message="Kubernetes API is reachable",
        )
    except Exception as e:
        return CheckResult(
            name="api_reachable",
            category="connectivity",
            passed=False,
            message="Cannot reach Kubernetes API",
            detail=str(e),
            fix_hint="Check your kubeconfig and network connectivity.",
        )


def _check_namespace_access(
    k8s: K8sClientProtocol, ns: str, prefix: str, domain: str
) -> CheckResult:
    """Check that the namespace exists or can be created.

    This check also attempts to create the namespace if it doesn't exist,
    because subsequent RBAC checks need the namespace to run.
    """
    try:
        if k8s.namespace_exists(ns):
            return CheckResult(
                name="namespace_access",
                category="connectivity",
                passed=True,
                message=f"Namespace '{ns}' is accessible",
                fixable=True,
            )
        # Attempt to create the namespace
        try:
            k8s.create_namespace(ns)
            return CheckResult(
                name="namespace_access",
                category="connectivity",
                passed=True,
                message=f"Namespace '{ns}' created",
                fixable=True,
            )
        except Exception:
            return CheckResult(
                name="namespace_access",
                category="connectivity",
                passed=False,
                message=f"Namespace '{ns}' does not exist and cannot be created",
                fixable=True,
                fix_hint=f"kubectl create namespace {ns}",
            )
    except Exception as e:
        return CheckResult(
            name="namespace_access",
            category="connectivity",
            passed=False,
            message=f"Cannot access namespace '{ns}'",
            detail=str(e),
            fixable=True,
            fix_hint=f"kubectl create namespace {ns}",
        )


def _fix_namespace(k8s: K8sClientProtocol, ns: str, prefix: str, domain: str) -> FixResult:
    """Create the namespace if it doesn't exist."""
    try:
        if k8s.namespace_exists(ns):
            return FixResult(
                check_name="namespace_access",
                fixed=True,
                message=f"Namespace '{ns}' already exists",
            )
        k8s.create_namespace(ns)
        return FixResult(
            check_name="namespace_access",
            fixed=True,
            message=f"Created namespace '{ns}'",
        )
    except Exception as e:
        return FixResult(
            check_name="namespace_access",
            fixed=False,
            message=f"Failed to create namespace '{ns}'",
            detail=str(e),
        )


def _check_configmap_crud(
    k8s: K8sClientProtocol, ns: str, prefix: str, domain: str
) -> CheckResult:
    """Check ConfigMap create/get/delete permissions."""
    probe_name = f"{prefix}-doctor-probe"
    try:
        body: dict[str, Any] = {
            "apiVersion": "v1",
            "kind": "ConfigMap",
            "metadata": {"name": probe_name},
            "data": {"probe": "ok"},
        }
        k8s.create_configmap(ns, body)
        k8s.get_configmap(ns, probe_name)
        k8s.delete_configmap(ns, probe_name)
        return CheckResult(
            name="configmap_crud",
            category="rbac",
            passed=True,
            message="ConfigMap create/get/delete works",
        )
    except Exception as e:
        with contextlib.suppress(Exception):
            k8s.delete_configmap(ns, probe_name)
        return CheckResult(
            name="configmap_crud",
            category="rbac",
            passed=False,
            message="ConfigMap CRUD failed",
            detail=str(e),
            fix_hint=(
                "Grant RBAC permissions: "
                "kubectl create clusterrolebinding <name> "
                "--clusterrole=admin --user=<user> --namespace=<ns>"
            ),
        )


def _check_secret_crud(k8s: K8sClientProtocol, ns: str, prefix: str, domain: str) -> CheckResult:
    """Check Secret create/get/delete permissions."""
    probe_name = f"{prefix}-doctor-probe-secret"
    try:
        body: dict[str, Any] = {
            "apiVersion": "v1",
            "kind": "Secret",
            "metadata": {"name": probe_name},
            "type": "Opaque",
            "data": {},
        }
        k8s.create_secret(ns, body)
        k8s.get_secret(ns, probe_name)
        k8s.delete_secret(ns, probe_name)
        return CheckResult(
            name="secret_crud",
            category="rbac",
            passed=True,
            message="Secret create/get/delete works",
        )
    except Exception as e:
        with contextlib.suppress(Exception):
            k8s.delete_secret(ns, probe_name)
        return CheckResult(
            name="secret_crud",
            category="rbac",
            passed=False,
            message="Secret CRUD failed",
            detail=str(e),
            fix_hint="Grant RBAC permissions for secrets in the target namespace.",
        )


def _check_deployment_crud(
    k8s: K8sClientProtocol, ns: str, prefix: str, domain: str
) -> CheckResult:
    """Check Deployment create/get/delete permissions."""
    probe_name = f"{prefix}-doctor-probe-deploy"
    try:
        body: dict[str, Any] = {
            "apiVersion": "apps/v1",
            "kind": "Deployment",
            "metadata": {"name": probe_name},
            "spec": {
                "replicas": 0,
                "selector": {"matchLabels": {"app": probe_name}},
                "template": {
                    "metadata": {"labels": {"app": probe_name}},
                    "spec": {"containers": [{"name": "probe", "image": "busybox:latest"}]},
                },
            },
        }
        k8s.create_deployment(ns, body)
        k8s.get_deployment(ns, probe_name)
        k8s.delete_deployment(ns, probe_name)
        return CheckResult(
            name="deployment_crud",
            category="rbac",
            passed=True,
            message="Deployment create/get/delete works",
        )
    except Exception as e:
        with contextlib.suppress(Exception):
            k8s.delete_deployment(ns, probe_name)
        return CheckResult(
            name="deployment_crud",
            category="rbac",
            passed=False,
            message="Deployment CRUD failed",
            detail=str(e),
            fix_hint="Grant RBAC permissions for deployments in the target namespace.",
        )


def _check_service_crud(k8s: K8sClientProtocol, ns: str, prefix: str, domain: str) -> CheckResult:
    """Check Service create/get/delete permissions."""
    probe_name = f"{prefix}-doctor-probe-svc"
    try:
        body: dict[str, Any] = {
            "apiVersion": "v1",
            "kind": "Service",
            "metadata": {"name": probe_name},
            "spec": {
                "selector": {"app": probe_name},
                "ports": [{"port": 80, "targetPort": 80, "protocol": "TCP"}],
            },
        }
        k8s.create_service(ns, body)
        k8s.get_service(ns, probe_name)
        k8s.delete_service(ns, probe_name)
        return CheckResult(
            name="service_crud",
            category="rbac",
            passed=True,
            message="Service create/get/delete works",
        )
    except Exception as e:
        with contextlib.suppress(Exception):
            k8s.delete_service(ns, probe_name)
        return CheckResult(
            name="service_crud",
            category="rbac",
            passed=False,
            message="Service CRUD failed",
            detail=str(e),
            fix_hint="Grant RBAC permissions for services in the target namespace.",
        )


def _check_network_policy_crud(
    k8s: K8sClientProtocol, ns: str, prefix: str, domain: str
) -> CheckResult:
    """Check NetworkPolicy create/get/delete permissions."""
    probe_name = f"{prefix}-doctor-probe-netpol"
    try:
        body: dict[str, Any] = {
            "apiVersion": "networking.k8s.io/v1",
            "kind": "NetworkPolicy",
            "metadata": {"name": probe_name},
            "spec": {
                "podSelector": {"matchLabels": {}},
                "policyTypes": ["Ingress"],
                "ingress": [],
            },
        }
        k8s.create_network_policy(ns, body)
        k8s.get_network_policy(ns, probe_name)
        k8s.delete_network_policy(ns, probe_name)
        return CheckResult(
            name="network_policy_crud",
            category="rbac",
            passed=True,
            message="NetworkPolicy create/get/delete works",
        )
    except Exception as e:
        with contextlib.suppress(Exception):
            k8s.delete_network_policy(ns, probe_name)
        return CheckResult(
            name="network_policy_crud",
            category="rbac",
            passed=False,
            message="NetworkPolicy CRUD failed (CNI may not support NetworkPolicy)",
            detail=str(e),
            fix_hint="Grant RBAC permissions for networkpolicies in the target namespace.",
        )


def _check_storage_class(k8s: K8sClientProtocol, ns: str, prefix: str, domain: str) -> CheckResult:
    """Check that a default StorageClass exists."""
    try:
        scs = k8s.list_storage_classes()
        if not scs:
            return CheckResult(
                name="storage_class",
                category="storage",
                passed=False,
                message="No StorageClass found (addons require persistent storage)",
                fix_hint="Install a storage provisioner (e.g., local-path-provisioner).",
            )
        has_default = any(
            sc.get("metadata", {})
            .get("annotations", {})
            .get("storageclass.kubernetes.io/is-default-class")
            == "true"
            for sc in scs
        )
        names = [sc.get("metadata", {}).get("name", "?") for sc in scs]
        if has_default:
            return CheckResult(
                name="storage_class",
                category="storage",
                passed=True,
                message=f"Default StorageClass exists ({', '.join(names)})",
            )
        return CheckResult(
            name="storage_class",
            category="storage",
            passed=False,
            message=f"No default StorageClass (found: {', '.join(names)})",
            detail="Set annotation storageclass.kubernetes.io/is-default-class=true on one SC",
            fix_hint="kubectl annotate storageclass <name> "
            "storageclass.kubernetes.io/is-default-class=true",
        )
    except Exception as e:
        return CheckResult(
            name="storage_class",
            category="storage",
            passed=False,
            message="Cannot list StorageClasses",
            detail=str(e),
            fix_hint="Grant RBAC permissions for storageclasses (cluster-scoped).",
        )


_KNOWN_CNI_DAEMONSETS = {"calico-node", "cilium", "kube-router", "weave-net"}


def _is_k3s(k8s: K8sClientProtocol) -> bool:
    """Detect k3s by checking node labels."""
    try:
        nodes = k8s.list_nodes()
        for node in nodes:
            labels = node.get("metadata", {}).get("labels", {})
            instance_type = labels.get("node.kubernetes.io/instance-type", "")
            if instance_type == "k3s":
                return True
    except Exception:
        pass
    return False


def _check_cni_enforcement(
    k8s: K8sClientProtocol, ns: str, prefix: str, domain: str
) -> CheckResult:
    """Check that a NetworkPolicy-capable CNI is installed."""
    try:
        # k3s has kube-router built-in (no visible DaemonSet)
        if _is_k3s(k8s):
            return CheckResult(
                name="cni_enforcement",
                category="infra",
                passed=True,
                message="NetworkPolicy CNI detected (k3s built-in kube-router)",
            )

        daemonsets = k8s.list_daemonsets("kube-system")
        ds_names = {ds.get("metadata", {}).get("name", "") for ds in daemonsets}
        found = _KNOWN_CNI_DAEMONSETS & ds_names
        if found:
            # Check if CNI pods are actually ready
            for ds in daemonsets:
                name = ds.get("metadata", {}).get("name", "")
                if name in found:
                    status = ds.get("status", {})
                    desired = status.get("desiredNumberScheduled", 0)
                    ready = status.get("numberReady", 0)
                    if desired > 0 and ready == 0:
                        return CheckResult(
                            name="cni_enforcement",
                            category="infra",
                            passed=False,
                            message=f"CNI '{name}' installed but not ready "
                            f"({ready}/{desired} pods)",
                            fixable=True,
                            fix_hint=(
                                f"Check CNI pod logs: "
                                f"kubectl logs -n kube-system -l k8s-app={name}"
                            ),
                        )
            return CheckResult(
                name="cni_enforcement",
                category="infra",
                passed=True,
                message=f"NetworkPolicy CNI detected ({', '.join(sorted(found))})",
            )

        return CheckResult(
            name="cni_enforcement",
            category="infra",
            passed=False,
            message="No known NetworkPolicy CNI detected",
            detail=(f"DaemonSets in kube-system: {', '.join(sorted(ds_names)) or '(none)'}"),
            fixable=True,
            fix_hint="Install a NetworkPolicy-capable CNI.",
        )
    except Exception as e:
        return CheckResult(
            name="cni_enforcement",
            category="infra",
            passed=False,
            message="Cannot check CNI DaemonSets in kube-system",
            detail=str(e),
            fix_hint="Grant list permissions on DaemonSets in kube-system namespace.",
        )


def _check_ingress_controller(
    k8s: K8sClientProtocol, ns: str, prefix: str, domain: str
) -> CheckResult:
    """Check that an Ingress controller is installed."""
    controller = detect_ingress_controller(k8s, ns)
    if controller is not None:
        detail = f"class={controller.class_name}"
        extra = ""
        if _is_k3s(k8s) and controller.name == "traefik":
            extra = ", k3s built-in"
        return CheckResult(
            name="ingress_controller",
            category="infra",
            passed=True,
            message=f"Ingress controller detected ({controller.name}{extra}, {detail})",
        )
    return CheckResult(
        name="ingress_controller",
        category="infra",
        passed=False,
        message="No Ingress controller detected",
        fix_hint="Install an Ingress controller (nginx-ingress, traefik, etc.).",
    )


def _check_ingress_crud(k8s: K8sClientProtocol, ns: str, prefix: str, domain: str) -> CheckResult:
    """Check Ingress create/get/delete permissions."""
    # Skip if no controller
    controller = detect_ingress_controller(k8s, ns)
    if controller is None:
        return CheckResult(
            name="ingress_crud",
            category="rbac",
            passed=True,
            message="Ingress create/get/delete — skipped (no controller)",
            detail="Install an Ingress controller first.",
        )

    probe_name = f"{prefix}-doctor-probe-ingress"
    try:
        body: dict[str, Any] = {
            "apiVersion": "networking.k8s.io/v1",
            "kind": "Ingress",
            "metadata": {"name": probe_name},
            "spec": {
                "rules": [
                    {
                        "host": "probe.test",
                        "http": {
                            "paths": [
                                {
                                    "path": "/",
                                    "pathType": "Prefix",
                                    "backend": {
                                        "service": {
                                            "name": "probe",
                                            "port": {"number": 80},
                                        }
                                    },
                                }
                            ]
                        },
                    }
                ]
            },
        }
        k8s.create_ingress(ns, body)
        k8s.get_ingress(ns, probe_name)
        k8s.delete_ingress(ns, probe_name)
        return CheckResult(
            name="ingress_crud",
            category="rbac",
            passed=True,
            message="Ingress create/get/delete works",
        )
    except Exception as e:
        with contextlib.suppress(Exception):
            k8s.delete_ingress(ns, probe_name)
        return CheckResult(
            name="ingress_crud",
            category="rbac",
            passed=False,
            message="Ingress CRUD failed",
            detail=str(e),
            fix_hint="Grant RBAC permissions for ingresses in the target namespace.",
        )


def _check_cert_manager(k8s: K8sClientProtocol, ns: str, prefix: str, domain: str) -> CheckResult:
    """Check that cert-manager is installed."""
    if detect_cert_manager(k8s):
        return CheckResult(
            name="cert_manager",
            category="infra",
            passed=True,
            message="cert-manager installed",
        )
    return CheckResult(
        name="cert_manager",
        category="infra",
        passed=False,
        message="cert-manager not detected (auto-TLS unavailable, use --no-tls)",
        fixable=True,
        fix_hint="Install cert-manager: kubectl apply -f "
        "https://github.com/cert-manager/cert-manager/releases/download/v1.14.0/cert-manager.yaml",
    )


def _check_rbac_permissions(
    k8s: K8sClientProtocol, ns: str, prefix: str, domain: str
) -> CheckResult:
    """Check RBAC permissions via SelfSubjectAccessReview (no side effects)."""
    report = check_rbac(k8s, ns)

    if report.all_required_allowed and report.all_elevated_allowed:
        return CheckResult(
            name="rbac_permissions",
            category="rbac",
            passed=True,
            message="All RBAC permissions granted",
        )

    denied_required = [p for p in report.permissions if p.required and not p.allowed]
    denied_elevated = [p for p in report.permissions if not p.required and not p.allowed]

    if denied_required:
        resources = sorted({p.resource for p in denied_required})
        return CheckResult(
            name="rbac_permissions",
            category="rbac",
            passed=False,
            message=f"Missing required RBAC permissions: {', '.join(resources)}",
            detail=f"{len(denied_required)} verb(s) denied on required resources",
            fixable=True,
            fix_hint="Run 'clusters:doctor --fix' to generate Role + RoleBinding YAML.",
        )

    # Only elevated missing — pass with detail
    resources = sorted({p.resource for p in denied_elevated})
    return CheckResult(
        name="rbac_permissions",
        category="rbac",
        passed=True,
        message=f"Required RBAC OK; elevated permissions missing: {', '.join(resources)}",
        detail=f"{len(denied_elevated)} verb(s) denied on optional resources",
    )


def _check_lb_support(k8s: K8sClientProtocol, ns: str, prefix: str, domain: str) -> CheckResult:
    """Check that LoadBalancer services work."""
    supported, desc = detect_lb_support(k8s)
    if supported:
        return CheckResult(
            name="lb_support",
            category="infra",
            passed=True,
            message=f"LoadBalancer support available ({desc})",
        )
    return CheckResult(
        name="lb_support",
        category="infra",
        passed=False,
        message=f"No LoadBalancer support ({desc})",
        fix_hint="Install MetalLB: kubectl apply -f "
        "https://raw.githubusercontent.com/metallb/metallb/v0.14.8/config/manifests/metallb-native.yaml",
    )


_CERT_MANAGER_MANIFEST = (
    "https://github.com/cert-manager/cert-manager/releases/download/v1.14.0/cert-manager.yaml"
)


def _fix_cert_manager(k8s: K8sClientProtocol, ns: str, prefix: str, domain: str) -> FixResult:
    """Install cert-manager via kubectl apply."""
    try:
        subprocess.run(
            ["kubectl", "apply", "-f", _CERT_MANAGER_MANIFEST],
            capture_output=True,
            check=True,
            timeout=120,
        )
        return FixResult(
            check_name="cert_manager",
            fixed=True,
            message="Installed cert-manager (pods may take a minute to start)",
        )
    except FileNotFoundError:
        return FixResult(
            check_name="cert_manager",
            fixed=False,
            message="kubectl not found",
            detail="Install kubectl and ensure it's on your PATH.",
        )
    except subprocess.CalledProcessError as e:
        return FixResult(
            check_name="cert_manager",
            fixed=False,
            message="Failed to install cert-manager",
            detail=e.stderr.decode().strip() if e.stderr else str(e),
        )
    except subprocess.TimeoutExpired:
        return FixResult(
            check_name="cert_manager",
            fixed=False,
            message="cert-manager install timed out (120s)",
        )


_CALICO_MANIFEST = (
    "https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/calico.yaml"
)


def _fix_cni(k8s: K8sClientProtocol, ns: str, prefix: str, domain: str) -> FixResult:
    """Install a NetworkPolicy-capable CNI.

    On k3s: NetworkPolicy is built-in, nothing to install.
    On other clusters: install Calico via kubectl apply.
    """
    # k3s already has kube-router built-in
    if _is_k3s(k8s):
        return FixResult(
            check_name="cni_enforcement",
            fixed=True,
            message="k3s has built-in NetworkPolicy support (kube-router)",
        )

    try:
        subprocess.run(
            ["kubectl", "apply", "-f", _CALICO_MANIFEST],
            capture_output=True,
            check=True,
            timeout=120,
        )
        return FixResult(
            check_name="cni_enforcement",
            fixed=True,
            message="Installed Calico CNI (pods may take a minute to start)",
        )
    except FileNotFoundError:
        return FixResult(
            check_name="cni_enforcement",
            fixed=False,
            message="kubectl not found",
            detail="Install kubectl and ensure it's on your PATH.",
        )
    except subprocess.CalledProcessError as e:
        return FixResult(
            check_name="cni_enforcement",
            fixed=False,
            message="Failed to install Calico CNI",
            detail=e.stderr.decode().strip() if e.stderr else str(e),
        )
    except subprocess.TimeoutExpired:
        return FixResult(
            check_name="cni_enforcement",
            fixed=False,
            message="Calico install timed out (120s)",
        )


# ── Registry ────────────────────────────────────────────────────

CLUSTER_REQUIREMENTS: tuple[ClusterRequirement, ...] = (
    ClusterRequirement(
        name="api_reachable",
        category="connectivity",
        description="K8s API server is reachable",
        check_fn=_check_api_reachable,
        fix_fn=None,
    ),
    ClusterRequirement(
        name="namespace_access",
        category="connectivity",
        description="Target namespace exists and is accessible",
        check_fn=_check_namespace_access,
        fix_fn=_fix_namespace,
    ),
    ClusterRequirement(
        name="configmap_crud",
        category="rbac",
        description="Can create/get/delete ConfigMaps",
        check_fn=_check_configmap_crud,
        fix_fn=None,
    ),
    ClusterRequirement(
        name="secret_crud",
        category="rbac",
        description="Can create/get/delete Secrets",
        check_fn=_check_secret_crud,
        fix_fn=None,
    ),
    ClusterRequirement(
        name="deployment_crud",
        category="rbac",
        description="Can create/get/delete Deployments",
        check_fn=_check_deployment_crud,
        fix_fn=None,
    ),
    ClusterRequirement(
        name="service_crud",
        category="rbac",
        description="Can create/get/delete Services",
        check_fn=_check_service_crud,
        fix_fn=None,
    ),
    ClusterRequirement(
        name="network_policy_crud",
        category="rbac",
        description="Can create/get/delete NetworkPolicies",
        check_fn=_check_network_policy_crud,
        fix_fn=None,
    ),
    ClusterRequirement(
        name="storage_class",
        category="storage",
        description="Default StorageClass exists",
        check_fn=_check_storage_class,
        fix_fn=None,
    ),
    ClusterRequirement(
        name="cni_enforcement",
        category="infra",
        description="NetworkPolicy-capable CNI is installed",
        check_fn=_check_cni_enforcement,
        fix_fn=_fix_cni,
    ),
    ClusterRequirement(
        name="ingress_controller",
        category="infra",
        description="Ingress controller detected",
        check_fn=_check_ingress_controller,
        fix_fn=None,
    ),
    ClusterRequirement(
        name="ingress_crud",
        category="rbac",
        description="Can create/get/delete Ingresses",
        check_fn=_check_ingress_crud,
        fix_fn=None,
    ),
    ClusterRequirement(
        name="cert_manager",
        category="infra",
        description="cert-manager installed for auto-TLS",
        check_fn=_check_cert_manager,
        fix_fn=_fix_cert_manager,
    ),
    ClusterRequirement(
        name="lb_support",
        category="infra",
        description="LoadBalancer support available",
        check_fn=_check_lb_support,
        fix_fn=None,
    ),
    ClusterRequirement(
        name="rbac_permissions",
        category="rbac",
        description="RBAC permissions for all kuberoku operations",
        check_fn=_check_rbac_permissions,
        fix_fn=None,
    ),
)


def run_all_checks(factory: KuberokuFactory) -> tuple[CheckResult, ...]:
    """Run all cluster requirement checks."""
    k8s = factory.k8s
    ns = factory.namespace
    prefix = factory.prefix
    domain = factory.domain
    return tuple(req.check_fn(k8s, ns, prefix, domain) for req in CLUSTER_REQUIREMENTS)


def fix_all(factory: KuberokuFactory) -> tuple[FixResult, ...]:
    """Attempt to fix all fixable failed checks."""
    k8s = factory.k8s
    ns = factory.namespace
    prefix = factory.prefix
    domain = factory.domain
    results: list[FixResult] = []
    for req in CLUSTER_REQUIREMENTS:
        if req.fix_fn is not None:
            check = req.check_fn(k8s, ns, prefix, domain)
            if not check.passed:
                result: FixResult = req.fix_fn(k8s, ns, prefix, domain)
                results.append(result)
    return tuple(results)
