from __future__ import annotations

import inspect
from threading import Lock
from typing import Any, Mapping

from flownet.core.request import FlownetPacket
from flownet.runtime.transport.transport_hub import TransportHub
from flownet.v1.runtime.comm.protocol import (
    OP_CONTROL_NODE_CALL,
    OP_CONTROL_NODE_RESULT,
    OP_RPC_ACTOR_CALL,
    OP_RPC_ACTOR_CALL_RESULT,
    OP_RPC_FLOW_PROGRAM_PULL,
    OP_RPC_FLOW_PROGRAM_PULL_RESULT,
    PLANE_CONTROL,
    SUPPORTED_OPS_BY_PLANE,
    SUPPORTED_PLANES,
    V1Envelope,
    coerce_envelope,
    make_envelope,
)
from flownet.v1.runtime.comm.transport import V1TransportReceiver


_V1_ENVELOPE_PACKET_TYPE = "service_event"
_REPLY_OP_BY_REQUEST_OP: dict[str, str] = {
    OP_RPC_ACTOR_CALL: OP_RPC_ACTOR_CALL_RESULT,
    OP_RPC_FLOW_PROGRAM_PULL: OP_RPC_FLOW_PROGRAM_PULL_RESULT,
    OP_CONTROL_NODE_CALL: OP_CONTROL_NODE_RESULT,
}


class V0TransportHubAdapter:
    """
    v1 transport backend on top of the v0 TransportHub stack.

    - outbound v1 envelope -> v0 FlownetPacket(type=service_event)
    - inbound v0 packet -> registered v1 endpoint receiver callback
    """

    def __init__(
        self,
        *,
        local_address: str,
        transport_hub: TransportHub,
        packet_type: str = _V1_ENVELOPE_PACKET_TYPE,
    ) -> None:
        self._local_address = _normalize_non_empty(local_address, field_name="local_address")
        self._transport_hub = transport_hub
        self._packet_type = _normalize_non_empty(packet_type, field_name="packet_type")
        self._receivers: dict[str, V1TransportReceiver] = {}
        self._lock = Lock()

    @property
    def local_address(self) -> str:
        return self._local_address

    @property
    def packet_type(self) -> str:
        return self._packet_type

    def register_endpoint(self, *, address: str, receiver: V1TransportReceiver) -> None:
        if not callable(receiver):
            raise TypeError("receiver must be callable.")
        normalized_address = _normalize_non_empty(address, field_name="address")
        if normalized_address != self._local_address:
            raise ValueError(
                "v0_transport_hub_adapter_address_mismatch:"
                f" expected={self._local_address},"
                f" actual={normalized_address}",
            )
        with self._lock:
            if normalized_address in self._receivers:
                raise ValueError(f"endpoint_already_registered:{normalized_address}")
            self._receivers[normalized_address] = receiver

    def unregister_endpoint(self, *, address: str) -> bool:
        normalized_address = _normalize_non_empty(address, field_name="address")
        with self._lock:
            return self._receivers.pop(normalized_address, None) is not None

    async def send(self, envelope_payload: V1Envelope | dict[str, Any]) -> None:
        envelope = coerce_envelope(envelope_payload)
        packet = FlownetPacket(
            type=self._packet_type,
            uuid=envelope.msg_id,
            reply=bool(envelope.expects_reply),
            packet_content=envelope.as_dict(),
        )
        await self._transport_hub.send_packet(envelope.target_address, packet)

    async def receive_packet(self, packet: FlownetPacket) -> None:
        if packet.type != self._packet_type:
            return
        receiver = self._resolve_receiver(self._local_address)
        if receiver is None:
            raise RuntimeError(f"v0_transport_adapter_endpoint_not_registered:{self._local_address}")
        try:
            result = receiver(packet.packet_content)
            if inspect.isawaitable(result):
                await result
        except Exception as exc:
            await self._send_receiver_error_reply(packet=packet, exc=exc)

    def endpoint_metadata(self) -> dict[str, Any]:
        selector = self._transport_hub.selector
        return {
            "host_id": selector.local_host_id,
            "uds_path": selector.local_uds_path,
            "shm_path": selector.local_shm_path,
            "transport_caps": list(selector.local_transport_caps),
        }

    async def _send_receiver_error_reply(self, *, packet: FlownetPacket, exc: Exception) -> None:
        if not bool(packet.reply):
            return
        context = _extract_reply_context(packet.packet_content)
        source_address = _normalize_optional_non_empty(context.get("source_address"))
        if source_address is None:
            return

        request_op = _normalize_optional_non_empty(context.get("op"))
        request_plane = _normalize_optional_non_empty(context.get("plane"))
        if request_plane not in SUPPORTED_PLANES:
            request_plane = PLANE_CONTROL
        reply_op = _REPLY_OP_BY_REQUEST_OP.get(request_op or "", request_op or OP_CONTROL_NODE_RESULT)
        allowed_ops = SUPPORTED_OPS_BY_PLANE.get(request_plane, frozenset())
        if reply_op not in allowed_ops:
            request_plane = PLANE_CONTROL
            reply_op = OP_CONTROL_NODE_RESULT

        reply_envelope = make_envelope(
            plane=request_plane,
            op=reply_op,
            source_address=self._local_address,
            target_address=source_address,
            expects_reply=False,
            request_ref_id=_normalize_optional_non_empty(context.get("request_ref_id")),
            body={
                "error_type": exc.__class__.__name__,
                "message": str(exc),
            },
            meta={
                "reply_to_msg_id": _normalize_non_empty(packet.uuid, field_name="packet.uuid"),
            },
        )
        await self.send(reply_envelope)

    def _resolve_receiver(self, address: str) -> V1TransportReceiver | None:
        with self._lock:
            return self._receivers.get(address)


def _extract_reply_context(payload: Any) -> dict[str, str | None]:
    if isinstance(payload, V1Envelope):
        return {
            "source_address": payload.source_address,
            "plane": payload.plane,
            "op": payload.op,
            "request_ref_id": payload.request_ref_id,
        }
    if not isinstance(payload, Mapping):
        return {}
    return {
        "source_address": _normalize_optional_non_empty(payload.get("source_address")),
        "plane": _normalize_optional_non_empty(payload.get("plane")),
        "op": _normalize_optional_non_empty(payload.get("op")),
        "request_ref_id": _normalize_optional_non_empty(payload.get("request_ref_id")),
    }


def _normalize_non_empty(value: Any, *, field_name: str) -> str:
    normalized = str(value or "").strip()
    if not normalized:
        raise ValueError(f"{field_name} must be non-empty.")
    return normalized


def _normalize_optional_non_empty(value: Any) -> str | None:
    if value is None:
        return None
    normalized = str(value).strip()
    if not normalized:
        return None
    return normalized


__all__ = ["V0TransportHubAdapter"]
