"""``citadel install`` — deploy the Citadel operator into a Kubernetes cluster.

All operations are idempotent: 409 Conflict on create → patch instead.
"""

from __future__ import annotations

import sys
import time
from typing import Any

from kubernetes import client, config
from kubernetes.client.rest import ApiException

from citadel_operator.cli._manifests import (
    build_cluster_role,
    build_cluster_role_binding,
    build_deployment,
    build_namespace,
    build_service_account,
    load_crds,
    load_default_policies,
    load_network_policies,
)

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _load_kube_config() -> None:
    """Load kubeconfig — try in-cluster first, fall back to local."""
    try:
        config.load_incluster_config()
    except config.ConfigException:
        config.load_kube_config()


def _apply_resource(
    api_call_create: Any,
    api_call_patch: Any,
    body: dict[str, Any],
    *,
    name: str | None = None,
    namespace: str | None = None,
) -> None:
    """Create-or-patch a resource.  409 Conflict triggers a patch."""
    try:
        if namespace is not None:
            api_call_create(namespace=namespace, body=body)
        else:
            api_call_create(body=body)
    except ApiException as exc:
        if exc.status == 409:
            resource_name = name or body["metadata"]["name"]
            if namespace is not None:
                api_call_patch(name=resource_name, namespace=namespace, body=body)
            else:
                api_call_patch(name=resource_name, body=body)
        else:
            raise


def _log(msg: str) -> None:
    print(f"  {msg}", file=sys.stderr)


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------


def install(
    namespace: str = "citadel-system",
    image: str = "citadel-operator:latest",
    with_default_policies: bool = True,
    with_network_policies: bool = False,
    timeout: int = 120,
) -> bool:
    """Install Citadel into the target cluster.  Returns True on success."""
    _load_kube_config()

    core = client.CoreV1Api()
    apps = client.AppsV1Api()
    rbac = client.RbacAuthorizationV1Api()
    ext = client.ApiextensionsV1Api()

    # 1. Namespace — wait if terminating from a previous uninstall
    _log(f"Creating namespace {namespace}...")
    ns_body = build_namespace(namespace)
    try:
        existing_ns = core.read_namespace(namespace)
        if existing_ns.metadata.deletion_timestamp is not None:
            _log(f"  Namespace {namespace} is terminating — waiting...")
            for _ in range(30):
                time.sleep(2)
                try:
                    core.read_namespace(namespace)
                except ApiException as e:
                    if e.status == 404:
                        break
                    raise
            else:
                _log(f"  WARNING: namespace {namespace} still terminating after 60s")
    except ApiException as exc:
        if exc.status != 404:
            raise
    try:
        core.create_namespace(body=ns_body)
    except ApiException as exc:
        if exc.status != 409:
            raise

    # 2. CRDs — wait for any terminating CRDs to clear first
    _log("Applying CRDs...")
    crd_names = []
    for crd in load_crds():
        crd_name = crd["metadata"]["name"]
        crd_names.append(crd_name)
        # If CRD exists and is terminating, wait for it to clear
        try:
            existing = ext.read_custom_resource_definition(crd_name)
            if existing.metadata.deletion_timestamp is not None:
                _log(f"  CRD {crd_name} is terminating — waiting...")
                for _ in range(30):
                    time.sleep(2)
                    try:
                        existing = ext.read_custom_resource_definition(crd_name)
                        if existing.metadata.deletion_timestamp is None:
                            break
                    except ApiException as e:
                        if e.status == 404:
                            break  # CRD deleted, we can proceed
                        raise
                else:
                    _log(f"  CRD {crd_name} stuck terminating — clearing finalizers")
                    existing.metadata.finalizers = None
                    ext.replace_custom_resource_definition(crd_name, existing)
                    time.sleep(2)
        except ApiException as exc:
            if exc.status != 404:
                raise
        # Now create or patch
        try:
            ext.create_custom_resource_definition(body=crd)
        except ApiException as exc:
            if exc.status == 409:
                ext.patch_custom_resource_definition(name=crd_name, body=crd)
            else:
                raise
    # Wait for CRDs to be established before creating custom resources
    for crd_name in crd_names:
        for _ in range(15):
            crd_obj = ext.read_custom_resource_definition(crd_name)
            conditions = crd_obj.status.conditions or []
            if any(c.type == "Established" and c.status == "True" for c in conditions):
                break
            time.sleep(1)
    _log("CRDs applied")

    # 3. ServiceAccount
    _log("Creating ServiceAccount...")
    sa = build_service_account(namespace)
    _apply_resource(
        core.create_namespaced_service_account,
        core.patch_namespaced_service_account,
        sa,
        namespace=namespace,
    )

    # 4. ClusterRole
    _log("Creating ClusterRole...")
    cr = build_cluster_role()
    _apply_resource(
        rbac.create_cluster_role,
        rbac.patch_cluster_role,
        cr,
    )

    # 5. ClusterRoleBinding
    _log("Creating ClusterRoleBinding...")
    crb = build_cluster_role_binding(namespace)
    _apply_resource(
        rbac.create_cluster_role_binding,
        rbac.patch_cluster_role_binding,
        crb,
    )

    # 6. Deployment
    _log("Creating Deployment...")
    dep = build_deployment(namespace, image)
    _apply_resource(
        apps.create_namespaced_deployment,
        apps.patch_namespaced_deployment,
        dep,
        namespace=namespace,
    )

    # 7. Default policies
    custom = client.CustomObjectsApi()
    if with_default_policies:
        _log("Applying default NodeTerminationPolicies...")
        for policy in load_default_policies():
            policy_name = policy["metadata"]["name"]
            try:
                custom.create_cluster_custom_object(
                    group="citadel.dev",
                    version="v1alpha1",
                    plural="nodeterminationpolicies",
                    body=policy,
                )
            except ApiException as exc:
                if exc.status == 409:
                    custom.patch_cluster_custom_object(
                        group="citadel.dev",
                        version="v1alpha1",
                        plural="nodeterminationpolicies",
                        name=policy_name,
                        body=policy,
                    )
                else:
                    raise
        _log(f"Applied {len(load_default_policies())} default policies")

    # 8. Network policies (Cilium CRDs — optional, requires Cilium installed)
    if with_network_policies:
        _log("Applying bundled Cilium network policies...")
        policies = load_network_policies()
        applied = 0
        for policy in policies:
            policy_name = policy["metadata"]["name"]
            kind = policy.get("kind", "")
            # CiliumClusterwideNetworkPolicy = cluster-scoped
            # CiliumNetworkPolicy = namespace-scoped (not used in bundled set)
            if kind == "CiliumClusterwideNetworkPolicy":
                try:
                    custom.create_cluster_custom_object(
                        group="cilium.io",
                        version="v2",
                        plural="ciliumclusterwidenetworkpolicies",
                        body=policy,
                    )
                    applied += 1
                except ApiException as exc:
                    if exc.status == 409:
                        custom.patch_cluster_custom_object(
                            group="cilium.io",
                            version="v2",
                            plural="ciliumclusterwidenetworkpolicies",
                            name=policy_name,
                            body=policy,
                        )
                        applied += 1
                    elif exc.status == 404:
                        _log("WARNING: Cilium CRDs not found — skipping network policies")
                        _log("  Install Cilium first, then re-run with --with-network-policies")
                        break
                    else:
                        raise
        if applied:
            _log(f"Applied {applied} network policies")

    # 9. Wait for rollout
    _log(f"Waiting for Deployment rollout (timeout {timeout}s)...")
    deadline = time.monotonic() + timeout
    while time.monotonic() < deadline:
        dep_status = apps.read_namespaced_deployment_status("citadel-operator", namespace)
        available = dep_status.status.available_replicas or 0
        if available >= 1:
            _log("Deployment ready")
            _print_summary(namespace, image, with_default_policies)
            return True
        time.sleep(3)

    _log("ERROR: Deployment did not become ready within timeout")
    return False


def _print_summary(namespace: str, image: str, with_policies: bool) -> None:
    print("\n  Citadel installed successfully!", file=sys.stderr)
    print(f"    Namespace:        {namespace}", file=sys.stderr)
    print(f"    Image:            {image}", file=sys.stderr)
    print(f"    Default policies: {'yes' if with_policies else 'no'}", file=sys.stderr)
    print(f"    Deployment:       citadel-operator", file=sys.stderr)
    print(f"    ServiceAccount:   citadel-operator", file=sys.stderr)
    print(f"    ClusterRole:      citadel-operator", file=sys.stderr)
    print("", file=sys.stderr)
