"""Namespace infrastructure operator for Citadel.

Kopf-based operator that watches NamespaceInfrastructure CRDs and reconciles
infrastructure resources (CiliumNetworkPolicies, ResourceQuotas, LimitRanges)
into the target namespace. Apollo creates the CRD; Citadel reconciles it.

Run standalone:
    kopf run --standalone -m citadel_operator.k3s.namespace_infra_operator
"""

import hashlib
import json
import logging
from datetime import UTC, datetime
from typing import Any

import kopf
from kubernetes import client
from kubernetes import config as k8s_config
from kubernetes.client.exceptions import ApiException

from citadel_operator.k3s.namespace_infra_builder import (
    build_default_deny_policy,
    build_limit_range,
    build_peer_policies,
    build_resource_quota,
    build_tenant_allow_policy,
)
from citadel_operator.k3s.namespace_infra_models import (
    InfraCondition,
    InfraPhase,
    ManagedInfraResources,
    NamespaceInfraSpec,
)

logger = logging.getLogger(__name__)

OPERATOR_NAME = "citadel-namespace-infra"
OPERATOR_VERSION = "0.1.0"
CRD_GROUP = "citadel.dev"
CRD_VERSION = "v1alpha1"
CRD_PLURAL = "namespaceinfrastructures"

MAX_CONDITIONS = 10


class NamespaceInfraReconciler:
    """Reconciles NamespaceInfrastructure CRDs into Kubernetes resources."""

    def __init__(self) -> None:
        self._core = client.CoreV1Api()
        self._custom = client.CustomObjectsApi()

    async def reconcile(
        self, body: dict[str, Any], name: str, namespace: str
    ) -> str | None:
        """Reconcile infrastructure resources for a NamespaceInfrastructure CRD.

        Returns error message on failure, None on success.
        """
        try:
            spec = NamespaceInfraSpec.from_dict(body.get("spec", {}))
        except (KeyError, ValueError) as e:
            msg = f"Failed to parse NamespaceInfrastructure spec: {e}"
            logger.error(msg)
            self._patch_status(
                name, namespace, phase=InfraPhase.FAILED, message=msg
            )
            return msg

        target_ns = spec.target_namespace

        self._patch_status(name, namespace, phase=InfraPhase.PROVISIONING)

        managed = ManagedInfraResources()

        try:
            # 1. Apply network policies
            np_config = spec.network_policy
            if np_config.default_deny:
                policy = build_default_deny_policy(target_ns, np_config)
                await self._apply_cilium_policy(policy)
                managed.default_deny_policy = True

            tenant_policy = build_tenant_allow_policy(target_ns, np_config)
            await self._apply_cilium_policy(tenant_policy)
            managed.tenant_policy = True

            for peer_policy in build_peer_policies(target_ns, np_config):
                await self._apply_cilium_policy(peer_policy)

            # 2. Apply resource quota
            if spec.resource_quota is not None:
                rq = build_resource_quota(target_ns, spec.resource_quota)
                if rq is not None:
                    await self._apply_namespaced_resource(
                        rq, target_ns, "resourcequotas"
                    )
                    managed.resource_quota = True

            # 3. Apply limit range
            if spec.limit_range is not None:
                lr = build_limit_range(target_ns, spec.limit_range)
                if lr is not None:
                    await self._apply_namespaced_resource(
                        lr, target_ns, "limitranges"
                    )
                    managed.limit_range = True

            # 4. Compute policy hash
            policy_hash = self._compute_policy_hash(spec)

            self._patch_status(
                name,
                namespace,
                phase=InfraPhase.ACTIVE,
                ready=True,
                message="All infrastructure resources reconciled",
                managed=managed,
                policy_hash=policy_hash,
            )
            return None

        except ApiException as e:
            msg = f"Kubernetes API error during reconciliation: {e.reason} ({e.status})"
            logger.error(msg)
            self._patch_status(
                name,
                namespace,
                phase=InfraPhase.FAILED,
                ready=False,
                message=msg,
                managed=managed,
            )
            return msg
        except Exception as e:
            msg = f"Unexpected error during reconciliation: {e}"
            logger.exception(msg)
            self._patch_status(
                name,
                namespace,
                phase=InfraPhase.FAILED,
                ready=False,
                message=msg,
                managed=managed,
            )
            return msg

    async def handle_delete(
        self, body: dict[str, Any], name: str, namespace: str
    ) -> None:
        """Clean up managed infrastructure resources in reverse order."""
        try:
            spec = NamespaceInfraSpec.from_dict(body.get("spec", {}))
        except (KeyError, ValueError) as e:
            logger.error(
                "Failed to parse spec during delete for %s: %s", name, e
            )
            return

        target_ns = spec.target_namespace

        # Delete in reverse order
        self._safe_delete_namespaced(target_ns, f"{target_ns}-limits", "limitranges")
        self._safe_delete_namespaced(target_ns, f"{target_ns}-quota", "resourcequotas")

        for peer_ns in spec.network_policy.allowed_peers:
            self._safe_delete_cilium_policy(target_ns, f"{target_ns}-peer-{peer_ns}")
        self._safe_delete_cilium_policy(target_ns, f"{target_ns}-tenant-allow")
        self._safe_delete_cilium_policy(target_ns, f"{target_ns}-default-deny")

    async def _apply_cilium_policy(self, policy: dict[str, Any]) -> None:
        """Create or update a CiliumNetworkPolicy."""
        name = policy["metadata"]["name"]
        namespace = policy["metadata"]["namespace"]

        try:
            self._custom.get_namespaced_custom_object(
                group="cilium.io",
                version="v2",
                namespace=namespace,
                plural="ciliumnetworkpolicies",
                name=name,
            )
            self._custom.patch_namespaced_custom_object(
                group="cilium.io",
                version="v2",
                namespace=namespace,
                plural="ciliumnetworkpolicies",
                name=name,
                body=policy,
            )
            logger.debug("Updated CiliumNetworkPolicy %s/%s", namespace, name)
        except ApiException as e:
            if e.status == 404:
                self._custom.create_namespaced_custom_object(
                    group="cilium.io",
                    version="v2",
                    namespace=namespace,
                    plural="ciliumnetworkpolicies",
                    body=policy,
                )
                logger.info("Created CiliumNetworkPolicy %s/%s", namespace, name)
            else:
                raise

    async def _apply_namespaced_resource(
        self,
        resource: dict[str, Any],
        namespace: str,
        resource_type: str,
    ) -> None:
        """Create or update a core namespaced resource."""
        name = resource["metadata"]["name"]
        api_method_map = {
            "resourcequotas": (
                self._core.read_namespaced_resource_quota,
                self._core.patch_namespaced_resource_quota,
                self._core.create_namespaced_resource_quota,
            ),
            "limitranges": (
                self._core.read_namespaced_limit_range,
                self._core.patch_namespaced_limit_range,
                self._core.create_namespaced_limit_range,
            ),
        }

        read_fn, patch_fn, create_fn = api_method_map[resource_type]

        try:
            read_fn(name=name, namespace=namespace)
            patch_fn(name=name, namespace=namespace, body=resource)
            logger.debug("Updated %s %s/%s", resource_type, namespace, name)
        except ApiException as e:
            if e.status == 404:
                create_fn(namespace=namespace, body=resource)
                logger.info("Created %s %s/%s", resource_type, namespace, name)
            else:
                raise

    def _compute_policy_hash(self, spec: NamespaceInfraSpec) -> str:
        """SHA-256 hash (first 16 chars) of network policy config for drift detection."""
        policy_data = json.dumps(spec.network_policy.to_dict(), sort_keys=True)
        return hashlib.sha256(policy_data.encode()).hexdigest()[:16]

    def _patch_status(
        self,
        name: str,
        namespace: str,
        phase: InfraPhase | None = None,
        ready: bool | None = None,
        message: str | None = None,
        managed: ManagedInfraResources | None = None,
        policy_hash: str | None = None,
    ) -> None:
        """Patch the CRD status subresource."""
        try:
            status_patch: dict[str, Any] = {}

            if phase is not None:
                status_patch["phase"] = phase.value
            if ready is not None:
                status_patch["ready"] = ready
            if managed is not None:
                status_patch["managedResources"] = managed.to_dict()
            if policy_hash is not None:
                status_patch["policyHash"] = policy_hash

            if message:
                condition = InfraCondition(
                    type="Ready" if ready else "Progressing",
                    status="True" if ready else "False",
                    message=message,
                    reason=phase.value if phase else "Unknown",
                    last_transition_time=datetime.now(UTC),
                )
                try:
                    current = self._custom.get_namespaced_custom_object(
                        group=CRD_GROUP,
                        version=CRD_VERSION,
                        namespace=namespace,
                        plural=CRD_PLURAL,
                        name=name,
                    )
                    existing = current.get("status", {}).get("conditions", [])
                except ApiException:
                    existing = []

                conditions = [InfraCondition.from_dict(c) for c in existing]
                conditions.append(condition)
                conditions = conditions[-MAX_CONDITIONS:]
                status_patch["conditions"] = [c.to_dict() for c in conditions]

                if phase == InfraPhase.FAILED:
                    status_patch["lastError"] = message

            if not status_patch:
                return

            self._custom.patch_namespaced_custom_object_status(
                group=CRD_GROUP,
                version=CRD_VERSION,
                namespace=namespace,
                plural=CRD_PLURAL,
                name=name,
                body={"status": status_patch},
            )
        except ApiException as e:
            logger.error("Failed to patch status for %s: %s", name, e)

    def _safe_delete_cilium_policy(self, namespace: str, name: str) -> None:
        """Delete a CiliumNetworkPolicy, ignoring 404."""
        try:
            self._custom.delete_namespaced_custom_object(
                group="cilium.io",
                version="v2",
                namespace=namespace,
                plural="ciliumnetworkpolicies",
                name=name,
            )
            logger.info("Deleted CiliumNetworkPolicy %s/%s", namespace, name)
        except ApiException as e:
            if e.status != 404:
                logger.error(
                    "Failed to delete CiliumNetworkPolicy %s/%s: %s",
                    namespace, name, e,
                )

    def _safe_delete_namespaced(
        self, namespace: str, name: str, resource_type: str
    ) -> None:
        """Delete a core namespaced resource, ignoring 404."""
        delete_map = {
            "resourcequotas": self._core.delete_namespaced_resource_quota,
            "limitranges": self._core.delete_namespaced_limit_range,
        }
        delete_fn = delete_map.get(resource_type)
        if delete_fn is None:
            return
        try:
            delete_fn(name=name, namespace=namespace)
            logger.info("Deleted %s %s/%s", resource_type, namespace, name)
        except ApiException as e:
            if e.status != 404:
                logger.error(
                    "Failed to delete %s %s/%s: %s",
                    resource_type, namespace, name, e,
                )


# === Global reconciler instance ===

_reconciler: NamespaceInfraReconciler | None = None


def get_reconciler() -> NamespaceInfraReconciler:
    """Get or create the global reconciler."""
    global _reconciler
    if _reconciler is None:
        _reconciler = NamespaceInfraReconciler()
    return _reconciler


# === Kopf Event Handlers ===


@kopf.on.create(CRD_GROUP, CRD_VERSION, CRD_PLURAL)
@kopf.on.update(CRD_GROUP, CRD_VERSION, CRD_PLURAL)
@kopf.on.resume(CRD_GROUP, CRD_VERSION, CRD_PLURAL)
async def on_reconcile(
    body: dict[str, Any],
    name: str,
    namespace: str,
    **kwargs,
) -> str | None:
    """Reconcile infrastructure resources on create/update/resume."""
    logger.info("Reconciling NamespaceInfrastructure %s/%s", namespace, name)
    reconciler = get_reconciler()
    error = await reconciler.reconcile(body, name, namespace)
    if error:
        raise kopf.TemporaryError(error, delay=30)
    return None


@kopf.on.delete(CRD_GROUP, CRD_VERSION, CRD_PLURAL)
async def on_delete(
    body: dict[str, Any],
    name: str,
    namespace: str,
    **kwargs,
) -> None:
    """Clean up managed infrastructure resources."""
    logger.info("Cleaning up NamespaceInfrastructure %s/%s", namespace, name)
    reconciler = get_reconciler()
    await reconciler.handle_delete(body, name, namespace)


@kopf.timer(
    CRD_GROUP,
    CRD_VERSION,
    CRD_PLURAL,
    interval=300,
)
async def on_drift_check(
    body: dict[str, Any],
    name: str,
    namespace: str,
    **kwargs,
) -> None:
    """Periodic drift detection — re-reconcile Active resources."""
    status = body.get("status", {})
    if status.get("phase") != InfraPhase.ACTIVE.value:
        return

    logger.debug("Drift check for NamespaceInfrastructure %s/%s", namespace, name)
    reconciler = get_reconciler()
    await reconciler.reconcile(body, name, namespace)


@kopf.on.startup()
async def on_startup(**kwargs) -> None:
    """Initialize the operator on startup."""
    logger.info("Starting Citadel Namespace Infrastructure Operator %s", OPERATOR_VERSION)

    try:
        k8s_config.load_incluster_config()
    except k8s_config.ConfigException:
        k8s_config.load_kube_config()

    get_reconciler()
    logger.info("Namespace Infrastructure Operator ready")


@kopf.on.probe(id="health")
async def health_probe(**kwargs) -> dict[str, Any]:
    """Health probe for liveness/readiness checks."""
    return {
        "operator": OPERATOR_NAME,
        "version": OPERATOR_VERSION,
        "timestamp": datetime.now(UTC).isoformat(),
    }
