from __future__ import annotations

import inspect
import os
import time
from typing import Awaitable, Callable
from typing import Any

from flownet.core.request import ActorCall, FlownetPacket
from flownet.runtime.node_control import NODE_CONTROL_PACKET_TYPE, node_control_error
from flownet.runtime.cluster_gossip_protocol import (
    MSG_DELTA_ACK,
    MSG_DELTA_PULL,
    MSG_DELTA_PUSH,
    MSG_SNAPSHOT,
)
from flownet.runtime.transport.call_manager import RemoteCallManager
from flownet.engine.flow_engine import FlowEngine
from flownet.runtime.actors.dispatcher import ActorDispatcher

CLUSTER_GOSSIP_PACKET_TYPES = {
    MSG_DELTA_PUSH,
    MSG_DELTA_PULL,
    MSG_DELTA_ACK,
    MSG_SNAPSHOT,
}
_REMOTE_ERROR_KEY = "__flownet_remote_error__"


def _protocol_debug_logs_enabled() -> bool:
    value = (os.environ.get("FLOWNET_PROTOCOL_DEBUG_LOGS") or "").strip().lower()
    return value in {"1", "true", "yes", "on"}


class RuntimeProtocol:
    def __init__(
        self,
        call_manager: RemoteCallManager,
        flow_engine: FlowEngine,
        actor_dispatcher: ActorDispatcher,
        service_registry: Any | None = None,
        topic_registry: Any | None = None,
        cluster_gossip_handler: Callable[[str, Any], Any] | None = None,
        node_control_handler: Callable[[Any], Any] | None = None,
        v1_envelope_handler: Callable[[FlownetPacket], Any] | None = None,
        cluster_metrics_snapshot: Callable[[], Any] | None = None,
        metrics_log_interval_s: float = 30.0,
        time_fn: Callable[[], float] | None = None,
        log_fn: Callable[[str], None] | None = None,
    ):
        self._call_manager = call_manager
        self._flow_engine = flow_engine
        self._actor_dispatcher = actor_dispatcher
        self._service_registry = service_registry
        self._topic_registry = topic_registry
        self._cluster_gossip_handler = cluster_gossip_handler
        self._node_control_handler = node_control_handler
        self._v1_envelope_handler = v1_envelope_handler
        self._cluster_metrics_snapshot = cluster_metrics_snapshot
        self._metrics_log_interval_s = max(0.0, float(metrics_log_interval_s))
        self._time_fn = time_fn or time.monotonic
        if log_fn is not None:
            self._log_fn = log_fn
        elif _protocol_debug_logs_enabled():
            self._log_fn = print
        else:
            self._log_fn = None
        self._last_metrics_log_at = float("-inf")
        self._stats: dict[str, int] = {
            "unknown_packet_type": 0,
            "actorcall_responses": 0,
            "node_control_responses": 0,
            "v1_envelope_packets": 0,
        }
        # Packet-family dispatch table (B4 phase-1):
        # keep one protocol entrypoint while separating domain handlers.
        self._packet_handlers: dict[
            str,
            Callable[[FlownetPacket], Awaitable[FlownetPacket | None]],
        ] = {
            "flowrequest": self._handle_flowrequest_packet,
            "actorcall": self._handle_actorcall_packet,
            "service_event": self._handle_service_event_packet,
            "topic_subscribe": self._handle_topic_subscribe_packet,
            "topic_unsubscribe": self._handle_topic_unsubscribe_packet,
            "topic_list": self._handle_topic_list_packet,
            "topic_event": self._handle_topic_event_packet,
            "topic_claim_producer": self._handle_topic_claim_producer,
            "topic_release_producer": self._handle_topic_release_producer,
            NODE_CONTROL_PACKET_TYPE: self._handle_node_control_packet,
        }
        if self._v1_envelope_handler is not None:
            self._packet_handlers["v1_envelope"] = self._handle_v1_envelope_packet
        for packet_type in CLUSTER_GOSSIP_PACKET_TYPES:
            self._packet_handlers[packet_type] = self._handle_cluster_gossip_packet

    def _log(self, message: str) -> None:
        log_fn = self._log_fn
        if log_fn is not None:
            log_fn(message)

    async def handle_packet(self, packet: FlownetPacket) -> FlownetPacket | None:
        try:
            handler = self._packet_handlers.get(packet.type)
            if handler is not None:
                return await handler(packet)
            self._stats["unknown_packet_type"] = self._stats.get("unknown_packet_type", 0) + 1
            self._log(f"[Runtime] Unknown packet type: {packet.type}")
            return None
        finally:
            self._maybe_emit_cluster_metrics()

    def stats_snapshot(self) -> dict[str, int]:
        return dict(self._stats)

    def _maybe_emit_cluster_metrics(self) -> None:
        if self._cluster_metrics_snapshot is None:
            return
        if self._metrics_log_interval_s <= 0.0:
            return
        now = self._time_fn()
        if (now - self._last_metrics_log_at) < self._metrics_log_interval_s:
            return
        self._last_metrics_log_at = now
        try:
            snapshot = self._cluster_metrics_snapshot()
        except Exception as exc:  # pragma: no cover - defensive branch
            self._log(f"[ClusterMetrics] snapshot failed: {exc}")
            return
        self._log(f"[ClusterMetrics] {snapshot}")

    async def _handle_flowrequest_packet(self, packet: FlownetPacket) -> FlownetPacket | None:
        request = packet.packet_content
        return await self._flow_engine.handle_flow_request(request)

    async def _handle_actorcall_packet(self, packet: FlownetPacket) -> FlownetPacket | None:
        actor_call: ActorCall = packet.packet_content
        try:
            result = await self._actor_dispatcher.handle_actor_call(
                actor_call.destination,
                *actor_call.payload.args,
                **actor_call.payload.kwargs,
            )
        except Exception as exc:
            result = {
                _REMOTE_ERROR_KEY: {
                    "type": exc.__class__.__name__,
                    "message": str(exc),
                }
            }
            self._stats["actorcall_responses"] = self._stats.get("actorcall_responses", 0) + 1
            self._log(f"[FlownetRuntime] Sending actor call error response: {result!r}")
        else:
            self._stats["actorcall_responses"] = self._stats.get("actorcall_responses", 0) + 1
            self._log(f"[FlownetRuntime] Sending actor call response: {result!r}")
        await self._call_manager.send_packet(
            address=actor_call.source,
            packet=FlownetPacket(
                type="response",
                uuid=packet.uuid,
                reply=False,
                packet_content=result,
            ),
        )
        return None

    async def _handle_service_event_packet(self, packet: FlownetPacket) -> FlownetPacket | None:
        if self._service_registry is not None:
            handler = getattr(self._service_registry, "handle_service_event", None)
            if handler is not None:
                handler(packet.packet_content)
        return None

    async def _handle_topic_subscribe_packet(self, packet: FlownetPacket) -> FlownetPacket | None:
        if self._topic_registry is None:
            return None
        payload = packet.packet_content or {}
        subscription = payload.get("subscription")
        if subscription is not None:
            self._topic_registry.register_subscription(subscription)
        await self._reply_to_packet(packet, payload, {"ok": True})
        return None

    async def _handle_topic_unsubscribe_packet(self, packet: FlownetPacket) -> FlownetPacket | None:
        if self._topic_registry is None:
            return None
        payload = packet.packet_content or {}
        subscription_id = payload.get("subscription_id")
        if subscription_id:
            self._topic_registry.unregister_subscription(subscription_id)
        await self._reply_to_packet(packet, payload, {"ok": True})
        return None

    async def _handle_topic_list_packet(self, packet: FlownetPacket) -> FlownetPacket | None:
        if self._topic_registry is None:
            return None
        payload = packet.packet_content or {}
        topic_id = payload.get("topic_id")
        epoch, subscriptions = (0, [])
        if topic_id:
            epoch, subscriptions = self._topic_registry.snapshot_subscriptions(topic_id)
        await self._reply_to_packet(
            packet,
            payload,
            {"epoch": epoch, "subscriptions": subscriptions},
        )
        return None

    async def _handle_topic_event_packet(self, packet: FlownetPacket) -> FlownetPacket | None:
        if self._topic_registry is None:
            return None
        payload = packet.packet_content or {}
        event = payload.get("event")
        if event is None:
            return None
        publish_meta = payload.get("publish_meta")
        if not isinstance(publish_meta, dict):
            publish_meta = {}
        publish_handler = getattr(self._topic_registry, "publish", None)
        if (
            callable(publish_handler)
            and hasattr(event, "source")
            and hasattr(event, "event_type")
            and hasattr(event, "payload")
        ):
            try:
                publish_handler(
                    event.source,
                    event.event_type,
                    event.payload,
                    getattr(event, "tags", None),
                    producer_id=publish_meta.get("producer_id"),
                    allow_reserved_namespace=bool(
                        publish_meta.get("allow_reserved_namespace", False)
                    ),
                    origin=getattr(event, "origin", None),
                )
            except TypeError:
                publish_handler(
                    event.source,
                    event.event_type,
                    event.payload,
                    getattr(event, "tags", None),
                )
            return None
        handler = getattr(self._topic_registry, "handle_topic_event", None)
        if handler is not None:
            handler(event)
        return None

    async def _handle_cluster_gossip_packet(self, packet: FlownetPacket) -> None:
        payload = packet.packet_content or {}
        if self._cluster_gossip_handler is None:
            result = {"ok": True, "ignored": True, "msg_type": packet.type}
        else:
            try:
                result = self._cluster_gossip_handler(packet.type, payload)
            except Exception as exc:
                self._log(f"[ClusterGossip] handler failed for {packet.type}: {exc}")
                result = {"ok": False, "error": str(exc), "msg_type": packet.type}

        if packet.reply and isinstance(payload, dict):
            reply_to = payload.get("reply_to")
            if reply_to:
                await self._call_manager.send_packet(
                    address=reply_to,
                    packet=FlownetPacket(
                        type="response",
                        uuid=packet.uuid,
                        reply=False,
                        packet_content=result,
                    ),
                )
        return None

    async def _handle_topic_claim_producer(self, packet: FlownetPacket) -> None:
        payload = packet.packet_content or {}
        topic_id = payload.get("topic_id")
        producer_id = payload.get("producer_id")
        policy = payload.get("policy")
        epoch = payload.get("epoch")
        ttl = payload.get("ttl")
        if not topic_id:
            result = {"ok": False, "error": "topic_id_required"}
            await self._reply_to_packet(packet, payload, result)
            return None
        if self._topic_registry is None:
            result = {"ok": False, "error": "topic_registry_not_configured"}
            await self._reply_to_packet(packet, payload, result)
            return None
        try:
            handler = getattr(self._topic_registry, "claim_producer_async", None)
            if handler is not None:
                result = await handler(
                    topic_id,
                    producer_id=producer_id,
                    policy=policy,
                    epoch=epoch,
                    ttl=ttl,
                )
            else:
                sync_handler = getattr(self._topic_registry, "claim_producer", None)
                if sync_handler is None:
                    result = {"ok": False, "error": "claim_producer_not_supported"}
                else:
                    result = sync_handler(
                        topic_id,
                        producer_id=producer_id,
                        policy=policy,
                        epoch=epoch,
                        ttl=ttl,
                    )
        except Exception as exc:
            result = {
                "ok": False,
                "error": str(exc),
                "topic_id": topic_id,
                "producer_id": producer_id,
            }
        await self._reply_to_packet(packet, payload, result)
        return None

    async def _handle_topic_release_producer(self, packet: FlownetPacket) -> None:
        payload = packet.packet_content or {}
        topic_id = payload.get("topic_id")
        producer_id = payload.get("producer_id")
        epoch = payload.get("epoch")
        if not topic_id:
            result = {"ok": False, "error": "topic_id_required"}
            await self._reply_to_packet(packet, payload, result)
            return None
        if self._topic_registry is None:
            result = {"ok": False, "error": "topic_registry_not_configured"}
            await self._reply_to_packet(packet, payload, result)
            return None
        try:
            handler = getattr(self._topic_registry, "release_producer_async", None)
            if handler is not None:
                result = await handler(
                    topic_id,
                    producer_id=producer_id,
                    epoch=epoch,
                )
            else:
                sync_handler = getattr(self._topic_registry, "release_producer", None)
                if sync_handler is None:
                    result = {"ok": False, "error": "release_producer_not_supported"}
                else:
                    result = sync_handler(
                        topic_id,
                        producer_id=producer_id,
                        epoch=epoch,
                    )
        except Exception as exc:
            result = {
                "ok": False,
                "error": str(exc),
                "topic_id": topic_id,
                "producer_id": producer_id,
            }
        await self._reply_to_packet(packet, payload, result)
        return None

    async def _handle_node_control_packet(self, packet: FlownetPacket) -> None:
        payload = packet.packet_content
        if not isinstance(payload, dict):
            payload = {}

        if self._node_control_handler is None:
            result = node_control_error(
                payload.get("op"),
                code="service_unavailable",
                message="Node control service is not configured.",
                error_type="RuntimeError",
            )
        else:
            try:
                handled = self._node_control_handler(payload)
                if inspect.isawaitable(handled):
                    handled = await handled
                if isinstance(handled, dict):
                    result = handled
                else:
                    result = node_control_error(
                        payload.get("op"),
                        code="invalid_response",
                        message=(
                            "Node control handler must return a mapping response; "
                            f"got {type(handled).__name__}."
                        ),
                        error_type="TypeError",
                    )
            except Exception as exc:
                result = node_control_error(
                    payload.get("op"),
                    code="internal_error",
                    message=str(exc),
                    error_type=exc.__class__.__name__,
                )

        self._stats["node_control_responses"] = self._stats.get("node_control_responses", 0) + 1
        await self._reply_to_packet(packet, payload, result)
        return None

    async def _handle_v1_envelope_packet(self, packet: FlownetPacket) -> None:
        handler = self._v1_envelope_handler
        if handler is None:
            return None
        self._stats["v1_envelope_packets"] = self._stats.get("v1_envelope_packets", 0) + 1
        result = handler(packet)
        if inspect.isawaitable(result):
            await result
        return None

    async def _reply_to_packet(
        self,
        packet: FlownetPacket,
        payload: dict[str, Any],
        result: Any,
    ) -> None:
        if not packet.reply:
            return
        if not isinstance(payload, dict):
            return
        reply_to = payload.get("reply_to")
        if not reply_to:
            return
        await self._call_manager.send_packet(
            address=reply_to,
            packet=FlownetPacket(
                type="response",
                uuid=packet.uuid,
                reply=False,
                packet_content=result,
            ),
        )
