"""GatewayService — shared LoadBalancer with allocated ports.

One shared LB IP, Kuberoku allocates unique ports (10000-10999).
Each mapping routes gateway_ip:allocated_port -> backend_service:target_port.

Backend strategies:
- nginx tcp-proxy: update tcp-services ConfigMap + patch nginx LB Service
- individual-lb: create individual LB Service per mapping (fallback)
"""

from __future__ import annotations

import json
from collections.abc import Callable
from typing import TYPE_CHECKING, Any

from kuberoku.exceptions import GatewayPortExhaustedError
from kuberoku.models import GatewayAllocation

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


PORT_MIN = 10000
PORT_MAX = 10999


class GatewayService:
    """Business logic for the shared gateway service."""

    def __init__(self, factory: KuberokuFactory) -> None:
        self._factory = factory

    @property
    def _k8s(self) -> K8sClientProtocol:
        return self._factory.k8s

    @property
    def _ns(self) -> str:
        return self._factory.namespace

    @property
    def _prefix(self) -> str:
        return self._factory.prefix

    @property
    def _domain(self) -> str:
        return self._factory.domain

    def _gateway_cm_name(self) -> str:
        return f"{self._prefix}-gateway"

    def allocate(
        self,
        app: str,
        target_service: str,
        target_port: int,
        on_step: Callable[[str], None] | None = None,
    ) -> GatewayAllocation:
        """Allocate a gateway port for a backend service.

        Uses optimistic concurrency with re-read collision guard.
        """
        if on_step:
            on_step("Allocating gateway port...")

        max_attempts = 5
        for _attempt in range(max_attempts):
            cm = self._get_or_create_gateway_cm()
            data = cm.get("data", {})
            allocations = self._parse_allocations(data.get("allocations", "{}"))
            next_port = int(data.get("next_port", str(PORT_MIN)))

            # Check if already allocated for this service
            for port_str, alloc in allocations.items():
                if alloc.get("service") == target_service and alloc.get("app") == app:
                    return GatewayAllocation(
                        external_port=int(port_str),
                        app=app,
                        target_service=target_service,
                        target_port=target_port,
                    )

            # Find next available port
            allocated_port = self._find_next_port(allocations, next_port)
            if allocated_port is None:
                raise GatewayPortExhaustedError()

            # Write allocation
            allocations[str(allocated_port)] = {
                "app": app,
                "service": target_service,
                "target_port": target_port,
            }
            cm["data"]["allocations"] = json.dumps(allocations, sort_keys=True)
            cm["data"]["next_port"] = str(allocated_port + 1)

            try:
                self._k8s.update_configmap(self._ns, self._gateway_cm_name(), cm)
            except Exception:
                # Conflict — retry with fresh data
                continue

            # Re-read and verify our allocation is still ours (collision guard)
            verified_cm = self._k8s.get_configmap(self._ns, self._gateway_cm_name())
            verified_allocs = self._parse_allocations(
                verified_cm.get("data", {}).get("allocations", "{}")
            )
            port_alloc = verified_allocs.get(str(allocated_port), {})
            if port_alloc.get("service") != target_service:
                # Another allocator claimed our port — retry
                continue

            # Apply backend: create individual LB Service
            if on_step:
                on_step(f"Mapping port {allocated_port} -> {target_service}:{target_port}...")
            self._apply_backend(allocated_port, target_service, target_port, app)

            return GatewayAllocation(
                external_port=allocated_port,
                app=app,
                target_service=target_service,
                target_port=target_port,
            )

        raise GatewayPortExhaustedError()

    def deallocate(
        self,
        app: str,
        target_service: str,
        on_step: Callable[[str], None] | None = None,
    ) -> None:
        """Remove a gateway allocation."""
        if on_step:
            on_step("Deallocating gateway port...")

        try:
            cm = self._k8s.get_configmap(self._ns, self._gateway_cm_name())
        except Exception:
            return

        data = cm.get("data", {})
        allocations = self._parse_allocations(data.get("allocations", "{}"))

        port_to_remove: str | None = None
        for port_str, alloc in allocations.items():
            if alloc.get("service") == target_service and alloc.get("app") == app:
                port_to_remove = port_str
                break

        if port_to_remove is None:
            return

        del allocations[port_to_remove]
        cm["data"]["allocations"] = json.dumps(allocations, sort_keys=True)
        self._k8s.update_configmap(self._ns, self._gateway_cm_name(), cm)

        # Remove backend
        self._remove_backend(int(port_to_remove))

    def list_allocations(self) -> list[GatewayAllocation]:
        """List all current gateway allocations."""
        try:
            cm = self._k8s.get_configmap(self._ns, self._gateway_cm_name())
        except Exception:
            return []

        data = cm.get("data", {})
        allocations = self._parse_allocations(data.get("allocations", "{}"))
        result: list[GatewayAllocation] = []
        for port_str, alloc in sorted(allocations.items()):
            result.append(
                GatewayAllocation(
                    external_port=int(port_str),
                    app=alloc.get("app", ""),
                    target_service=alloc.get("service", ""),
                    target_port=alloc.get("target_port", 0),
                )
            )
        return result

    def get_allocation(
        self,
        app: str,
        target_service: str,
    ) -> GatewayAllocation | None:
        """Get the allocation for a specific service, if any."""
        try:
            cm = self._k8s.get_configmap(self._ns, self._gateway_cm_name())
        except Exception:
            return None

        data = cm.get("data", {})
        allocations = self._parse_allocations(data.get("allocations", "{}"))
        for port_str, alloc in allocations.items():
            if alloc.get("service") == target_service and alloc.get("app") == app:
                return GatewayAllocation(
                    external_port=int(port_str),
                    app=app,
                    target_service=target_service,
                    target_port=alloc.get("target_port", 0),
                )
        return None

    def get_gateway_ip(self) -> str | None:
        """Get the gateway's external IP."""
        # Look for individual LB services
        gw_prefix = f"{self._prefix}-gw-"
        services = self._k8s.list_services(self._ns)
        for svc in services:
            name = svc.get("metadata", {}).get("name", "")
            if name.startswith(gw_prefix):
                status = svc.get("status", {})
                lb = status.get("loadBalancer", {})
                for ing in lb.get("ingress", []):
                    if ing.get("ip"):
                        return str(ing["ip"])
                    if ing.get("hostname"):
                        return str(ing["hostname"])
        return None

    # ── Private helpers ──────────────────────────────────────────

    def _get_or_create_gateway_cm(self) -> dict[str, Any]:
        """Get or create the gateway ConfigMap."""
        cm_name = self._gateway_cm_name()
        try:
            return self._k8s.get_configmap(self._ns, cm_name)
        except Exception:
            body: dict[str, Any] = {
                "apiVersion": "v1",
                "kind": "ConfigMap",
                "metadata": {
                    "name": cm_name,
                    "labels": {
                        f"{self._domain}/managed-by": self._prefix,
                        f"{self._domain}/resource-type": "gateway",
                    },
                },
                "data": {
                    "allocations": "{}",
                    "next_port": str(PORT_MIN),
                },
            }
            return self._k8s.create_configmap(self._ns, body)

    @staticmethod
    def _parse_allocations(raw: str) -> dict[str, Any]:
        try:
            return dict(json.loads(raw))
        except (json.JSONDecodeError, TypeError):
            return {}

    @staticmethod
    def _find_next_port(
        allocations: dict[str, Any],
        start: int,
    ) -> int | None:
        """Find the next available port in the range."""
        used = {int(p) for p in allocations}
        for port in range(start, PORT_MAX + 1):
            if port not in used:
                return port
        # Wrap around
        for port in range(PORT_MIN, start):
            if port not in used:
                return port
        return None

    def _apply_backend(
        self,
        external_port: int,
        target_service: str,
        target_port: int,
        app: str,
    ) -> None:
        """Create individual LB Service for gateway mapping."""
        # Copy selector from target service so the LB can route to pods
        selector: dict[str, str] = {}
        try:
            target_svc = self._k8s.get_service(self._ns, target_service)
            selector = dict(target_svc.get("spec", {}).get("selector", {}))
        except Exception:
            pass

        svc_name = f"{self._prefix}-gw-{external_port}"
        svc_body: dict[str, Any] = {
            "apiVersion": "v1",
            "kind": "Service",
            "metadata": {
                "name": svc_name,
                "labels": {
                    f"{self._domain}/managed-by": self._prefix,
                    f"{self._domain}/resource-type": "gateway",
                    f"{self._domain}/app": app,
                },
            },
            "spec": {
                "type": "LoadBalancer",
                "ports": [
                    {
                        "port": external_port,
                        "targetPort": target_port,
                        "protocol": "TCP",
                    }
                ],
                "selector": selector,
            },
        }
        try:
            self._k8s.create_service(self._ns, svc_body)
        except Exception as create_err:
            # Only tolerate "already exists" — verify by fetching
            try:
                self._k8s.get_service(self._ns, svc_name)
            except Exception:
                raise create_err from None

    def _remove_backend(self, external_port: int) -> None:
        """Remove individual LB Service for gateway mapping."""
        svc_name = f"{self._prefix}-gw-{external_port}"
        try:
            self._k8s.delete_service(self._ns, svc_name)
        except Exception as delete_err:
            # Only tolerate "already gone" — verify it's actually missing
            try:
                self._k8s.get_service(self._ns, svc_name)
            except Exception:
                return  # Confirmed gone
            raise delete_err from None
