from __future__ import annotations

import inspect
import uuid
from dataclasses import dataclass
from typing import Any

from flownet.api.actor_method_ref import ActorMethodRef
from flownet.core.request import (
    ActorCall,
    FlowRequest,
    FlownetPacket,
    is_stream_tracker_ack_coordinator_target,
)
from flownet.core.types import PayloadData
from flownet.engine.port import EnginePort
from flownet.runtime.contexts.flow import flow_context_scope
from flownet.runtime.kernel import RuntimeKernel, get_runtime_kernel
from flownet.runtime.node_control import NodeControlCallError


_ACK_CONTROL_COMPAT_FALLBACK_CODES = frozenset(
    {
        "unsupported_op",
        "service_unavailable",
    }
)


class _FlowScopedGenerator:
    def __init__(self, generator, request: FlowRequest, actor_ctx: Any):
        self._generator = generator
        self._request = request
        self._actor_ctx = actor_ctx

    def __iter__(self):
        return self

    def __next__(self):
        return self._with_scope(self._generator.__next__)

    def send(self, value):
        return self._with_scope(self._generator.send, value)

    def throw(self, *args):
        return self._with_scope(self._generator.throw, *args)

    def close(self):
        return self._with_scope(self._generator.close)

    def _with_scope(self, fn, *args):
        with flow_context_scope(self._request, actor_ctx=self._actor_ctx):
            return fn(*args)


class _FlowScopedAsyncGenerator:
    def __init__(self, async_generator, request: FlowRequest, actor_ctx: Any):
        self._async_generator = async_generator
        self._request = request
        self._actor_ctx = actor_ctx

    def __aiter__(self):
        return self

    async def __anext__(self):
        return await self._with_scope(self._async_generator.__anext__)

    async def asend(self, value):
        return await self._with_scope(self._async_generator.asend, value)

    async def athrow(self, *args):
        return await self._with_scope(self._async_generator.athrow, *args)

    async def aclose(self):
        return await self._with_scope(self._async_generator.aclose)

    async def _with_scope(self, fn, *args):
        with flow_context_scope(self._request, actor_ctx=self._actor_ctx):
            return await fn(*args)


def _wrap_flow_scoped_result(result: Any, request: FlowRequest, actor_ctx: Any) -> Any:
    if inspect.isgenerator(result):
        return _FlowScopedGenerator(result, request, actor_ctx)
    if inspect.isasyncgen(result):
        return _FlowScopedAsyncGenerator(result, request, actor_ctx)
    return result


def _is_ack_control_compat_error(exc: Exception) -> bool:
    if isinstance(exc, NodeControlCallError):
        return str(exc.code) in _ACK_CONTROL_COMPAT_FALLBACK_CODES
    if isinstance(exc, RuntimeError):
        message = str(exc).strip().lower()
        if "node control" in message and ("not configured" in message or "unavailable" in message):
            return True
        if "unsupported" in message and "node control" in message:
            return True
    return False


@dataclass(frozen=True)
class FlowEngineRuntimeAdapter(EnginePort):
    kernel: RuntimeKernel

    def local_address(self) -> str:
        return self.kernel.local_address()

    async def forward_flowrequest(self, address: str, request: FlowRequest) -> Any:
        # ARCH_NOTE:
        # - Packets forward the full FlowRequest object (including frames), so
        #   request execution is currently driven by run materialization rather
        #   than a separately resolved persistent flow definition ID.
        io = self.kernel.require_io()
        return await io.send_packet(
            address,
            FlownetPacket(
                type="flowrequest",
                uuid=uuid.uuid4().hex,
                reply=False,
                packet_content=request,
            ),
        )

    def invoke_local(
        self,
        rmf: ActorMethodRef,
        payload: PayloadData,
        request: FlowRequest,
    ) -> Any:
        from flownet.runtime.contexts.actor import _build_actor_context, _user_execution_scope

        actor = self.kernel.require_actors().resolve(rmf.actor_id)
        method = getattr(actor, rmf.method, None)
        if method is None:
            raise ValueError(f"Method {rmf.method} not found on {rmf.actor_id}")
        actor_ctx = _build_actor_context(self.kernel)
        with flow_context_scope(request, actor_ctx=actor_ctx):
            if rmf.actor_id.startswith("__"):
                result = method(*payload.args, **payload.kwargs)
                return _wrap_flow_scoped_result(result, request, actor_ctx)
            with _user_execution_scope(None):
                result = method(*payload.args, **payload.kwargs)
                return _wrap_flow_scoped_result(result, request, actor_ctx)

    async def handle_actor_call(self, rmf: ActorMethodRef, *args, **kwargs) -> Any:
        return await self.kernel.require_actors().call(rmf, *args, **kwargs)

    async def send_ack(
        self,
        origin_address: str,
        coordinator_target: str,
        request_id: str,
        delta: int,
    ) -> None:
        if is_stream_tracker_ack_coordinator_target(coordinator_target):
            await self._send_stream_tracker_ack(
                origin_address,
                request_id,
                delta,
            )
            return

        try:
            control = self.kernel.require_node_control()
        except Exception as exc:
            if _is_ack_control_compat_error(exc):
                raise RuntimeError("event_chain_request_done control path unavailable.") from exc
            raise

        if origin_address == self.local_address():
            try:
                result = control.invoke(
                    "event_chain_request_done",
                    coordinator_target,
                    request_id,
                    delta,
                )
                if inspect.isawaitable(result):
                    await result
                return
            except Exception as exc:
                if _is_ack_control_compat_error(exc):
                    raise RuntimeError("event_chain_request_done control path unavailable.") from exc
                raise

        try:
            await control.call_on(
                origin_address,
                "event_chain_request_done",
                coordinator_target,
                request_id,
                delta,
            )
            return
        except Exception as exc:
            if _is_ack_control_compat_error(exc):
                raise RuntimeError("event_chain_request_done control path unavailable.") from exc
            raise

    async def _send_stream_tracker_ack(
        self,
        origin_address: str,
        request_id: str,
        delta: int,
    ) -> None:
        if origin_address == self.local_address():
            control = self.kernel.require_node_control()
            try:
                result = control.invoke(
                    "stream_tracker_event_chain_done",
                    request_id,
                    delta,
                )
                if inspect.isawaitable(result):
                    await result
                return
            except Exception as exc:
                if _is_ack_control_compat_error(exc):
                    raise RuntimeError("stream_tracker_event_chain_done control path unavailable.") from exc
                raise

        control = self.kernel.require_node_control()
        try:
            await control.call_on(
                origin_address,
                "stream_tracker_event_chain_done",
                request_id,
                delta,
            )
            return
        except Exception as exc:
            if _is_ack_control_compat_error(exc):
                raise RuntimeError("stream_tracker_event_chain_done control path unavailable.") from exc
            raise

    async def send_flow_error(
        self,
        origin_address: str,
        actor_id: str,
        request_id: str,
        *,
        error_type: str,
        message: str,
        traceback_text: str,
        event: Any | None = None,
    ) -> None:
        if origin_address == self.local_address():
            try:
                actor = self.kernel.require_actors().resolve(actor_id)
            except Exception:
                return
            method = getattr(actor.object, "flow_error", None)
            if method is None:
                return
            method(request_id, error_type, message, traceback_text, event)
            return

        io = self.kernel.require_io()
        await io.send_packet(
            origin_address,
            FlownetPacket(
                type="actorcall",
                uuid=uuid.uuid4().hex,
                reply=False,
                packet_content=ActorCall(
                    source=self.local_address(),
                    destination=ActorMethodRef(
                        address=origin_address,
                        actor_id=actor_id,
                        method="flow_error",
                    ),
                    payload=PayloadData(
                        args=[request_id, error_type, message, traceback_text, event],
                        kwargs={},
                    ),
                ),
            ),
        )

    async def resolve_stateless_op(self, op_ref: Any) -> Any:
        from flownet.runtime.stateless_ops import resolve_stateless_op

        return await resolve_stateless_op(op_ref)

    def submit_runtime_coro(self, coro: Any) -> Any:
        return self.kernel.require_tasks().submit_runtime_coro(coro)


def build_engine_port(kernel: RuntimeKernel | None = None) -> EnginePort:
    if kernel is None:
        kernel = get_runtime_kernel()
    return FlowEngineRuntimeAdapter(kernel)


__all__ = ["FlowEngineRuntimeAdapter", "build_engine_port"]
