from __future__ import annotations

import asyncio
from collections.abc import Coroutine
from typing import Any, Callable

from flownet.v1.runtime.actors.actor_api import ActorAPI
from flownet.v1.runtime.comm import OP_RPC_ACTOR_CALL, PLANE_RPC, V1CommHub


def build_actor_exception_handler_invoker(
    *,
    actor_api: ActorAPI,
    comm_hub: V1CommHub | None = None,
    timeout: float | None = None,
    run_coroutine: Callable[[Coroutine[Any, Any, Any]], Any] | None = None,
) -> Callable[[Any, dict[str, Any]], Any]:
    """
    Build a sync exception-handler invoker over v1 actor call plane.

    Local handler target:
    - resolved by ActorAPI and executed locally.

    Remote handler target:
    - encoded as actor call intent and sent via V1CommHub rpc plane.
    """

    def _runner(coro: Coroutine[Any, Any, Any]) -> Any:
        if callable(run_coroutine):
            return run_coroutine(coro)
        try:
            asyncio.get_running_loop()
        except RuntimeError:
            return asyncio.run(coro)
        raise RuntimeError(
            "actor_exception_handler_invoker_requires_run_coroutine_when_called_inside_running_event_loop",
        )

    async def _invoke_async(handler_target: Any, event: dict[str, Any]) -> Any:
        prepared = await actor_api.call_or_prepare_call(handler_target, event)
        mode = _normalize_non_empty(prepared.get("mode"), field_name="mode")
        if mode == "local_result":
            return prepared.get("result")
        if mode != "actor_call_intent":
            raise RuntimeError(f"unsupported_actor_call_prepare_mode:{mode}")

        if comm_hub is None:
            raise RuntimeError("remote_exception_handler_requires_comm_hub")

        target_address = _normalize_non_empty(
            prepared.get("address"),
            field_name="address",
        )
        reply = await comm_hub.request(
            plane=PLANE_RPC,
            op=OP_RPC_ACTOR_CALL,
            target_address=target_address,
            timeout=timeout,
            request_ref_id=_resolve_request_ref_id(event),
            body=prepared,
        )
        body = reply.body
        if not isinstance(body, dict):
            raise TypeError("exception_handler_actor_call_reply_body must be a dict.")
        reply_mode = _normalize_non_empty(body.get("mode"), field_name="mode")
        if reply_mode == "applied_actor_call":
            return body.get("result")
        if reply_mode == "applied_actor_call_error":
            error_type = _normalize_non_empty(body.get("error_type"), field_name="error_type")
            message = _normalize_non_empty(body.get("message"), field_name="message")
            raise RuntimeError(f"exception_handler_actor_call_failed:{error_type}:{message}")
        raise RuntimeError(f"unsupported_actor_call_reply_mode:{reply_mode}")

    def _invoke(handler_target: Any, event: dict[str, Any]) -> Any:
        if not isinstance(event, dict):
            raise TypeError("exception event must be a dict.")
        coro = _invoke_async(handler_target, event)
        try:
            return _runner(coro)
        except Exception:
            try:
                coro.close()
            except Exception:
                pass
            raise

    return _invoke


def _resolve_request_ref_id(event: dict[str, Any]) -> str | None:
    raw = event.get("event_group_id")
    if raw is None:
        return None
    normalized = str(raw).strip()
    if not normalized:
        return None
    return normalized


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


__all__ = ["build_actor_exception_handler_invoker"]
