from __future__ import annotations

import asyncio
from collections.abc import Coroutine
from concurrent.futures import CancelledError as FutureCancelledError
from concurrent.futures import Future
from concurrent.futures import TimeoutError as FutureTimeoutError
from typing import Any

from flownet.v1.runtime.actors import (
    ActorAPI,
    ExecutorLanes,
    call_actor_via_comm,
    register_actor_call_rpc_handler,
)
from flownet.v1.runtime.actors.callback_registry import (
    CALLBACK_REGISTRY_SERVICE_ACTOR_ID,
    ActorCallbackRuntime,
    CallbackRegistry,
    CallbackRegistryServiceActor,
)
from flownet.v1.runtime.comm import (
    OP_CONTROL_TOPIC_EVENT_CHAIN_DONE_FORWARD,
    OP_CONTROL_TOPIC_PRODUCER_DONE_FORWARD,
    OP_CONTROL_TOPIC_REQUEST_DONE_NOTIFY,
    OP_DATA_TOPIC_EVENT_FORWARD,
    OP_RPC_ACTOR_CALL,
    OP_RPC_FLOW_PROGRAM_PULL,
    PLANE_CONTROL,
    PLANE_DATA,
    PLANE_RPC,
    V1CommHub,
    V1TransportBackend,
)
from flownet.v1.runtime.flowengine import (
    FlowEngineV1,
    FlowProcessExecution,
    build_flow_program_pull_resolver,
    register_flow_program_pull_handler,
)
from flownet.v1.runtime.loops import LoopThread
from flownet.v1.runtime.topics import (
    FlowProgramRoutingDirectory,
    TopicAPI,
    build_actor_exception_handler_invoker,
    register_topic_forward_handlers,
    send_topic_forward_intent_via_comm,
)


class V1RuntimeHost:
    """
    Minimal v1 runtime host.

    Owns loop threads and composes v1 ActorAPI + TopicAPI into one runtime entry.
    """

    def __init__(
        self,
        *,
        local_address: str,
        actor_api: ActorAPI | None = None,
        comm_hub: V1CommHub | None = None,
        comm_transport: V1TransportBackend | None = None,
        topic_api: TopicAPI | None = None,
        executor_lanes: ExecutorLanes | None = None,
        runtime_loop: LoopThread | None = None,
        user_loop: LoopThread | None = None,
        default_cpu_max_workers: int | None = None,
        exception_handler_timeout: float | None = None,
        gc_idle_interval_seconds: float | None = 30.0,
        gc_idle_ttl_seconds: float = 300.0,
    ) -> None:
        if comm_hub is not None and comm_transport is not None:
            raise ValueError("comm_hub and comm_transport cannot both be provided.")

        normalized_gc_interval_seconds = self._normalize_optional_interval_seconds(
            gc_idle_interval_seconds,
            field_name="gc_idle_interval_seconds",
        )
        normalized_gc_ttl_seconds = self._normalize_non_negative_seconds(
            gc_idle_ttl_seconds,
            field_name="gc_idle_ttl_seconds",
        )

        self._runtime_loop = runtime_loop or LoopThread("flownet-v1-runtime-loop")
        self._user_loop = user_loop or LoopThread("flownet-v1-user-loop")
        self._owns_runtime_loop = runtime_loop is None
        self._owns_user_loop = user_loop is None
        self._closed = False
        self._gc_idle_interval_seconds = normalized_gc_interval_seconds
        self._gc_idle_ttl_seconds = normalized_gc_ttl_seconds
        self._gc_loop_future: Future | None = None
        self._callback_runtime: ActorCallbackRuntime | None = None

        if actor_api is not None and executor_lanes is not None:
            raise ValueError("actor_api and executor_lanes cannot both be provided.")

        self._owns_executor_lanes = False
        self.executor_lanes: ExecutorLanes | None = executor_lanes
        if actor_api is None:
            if self.executor_lanes is None:
                self.executor_lanes = ExecutorLanes(
                    default_cpu_max_workers=default_cpu_max_workers,
                )
                self._owns_executor_lanes = True
            self.actor_api = ActorAPI(
                local_address=local_address,
                executor_lanes=self.executor_lanes,
            )
            self._owns_actor_api = True
        else:
            self.actor_api = actor_api
            self._owns_actor_api = False
            self.executor_lanes = None

        self.comm_hub = comm_hub or V1CommHub(
            local_address=local_address,
            transport=comm_transport,
        )
        self._owns_comm_hub = comm_hub is None
        self._ensure_actor_call_rpc_handler()
        invoke_exception_handler = build_actor_exception_handler_invoker(
            actor_api=self.actor_api,
            comm_hub=self.comm_hub,
            timeout=exception_handler_timeout,
            run_coroutine=self._run_user_coro_blocking,
        )

        if topic_api is None:
            flow_program_routing_directory = FlowProgramRoutingDirectory()
            pull_flow_program = build_flow_program_pull_resolver(
                comm_hub=self.comm_hub,
                resolve_owner_address=(
                    lambda flow_program_ref: flow_program_routing_directory.require(
                        flow_program_ref
                    ).owner_address
                ),
                run_coroutine=self._run_user_coro_blocking,
            )
            self.topic_api = TopicAPI(
                local_address=local_address,
                flow_program_routing_directory=flow_program_routing_directory,
                pull_flow_program=pull_flow_program,
            )
            self._owns_topic_api = True
        else:
            self.topic_api = topic_api
            self._owns_topic_api = False
        self._ensure_topic_forward_handlers()
        self._ensure_callback_runtime()
        self.actor_api.set_event_dispatcher(self._dispatch_actor_event)
        if not self.topic_api.has_ingress_event_handler():
            flow_process_execution = FlowProcessExecution(
                flow_process_catalog=self.topic_api.flow_process_catalog,
                flow_engine=FlowEngineV1(),
                resolve_flow_program=self.topic_api.resolve_flow_program_by_ref,
                resolve_topic_route=self.topic_api.require_topic_route,
                publish_flow_output=self.topic_api.publish_flow_output,
                apply_or_forward_event_chain_delta=self.topic_api.apply_or_forward_event_chain_delta,
                invoke_exception_handler=invoke_exception_handler,
            )
            self.topic_api.set_ingress_event_handler(flow_process_execution.dispatch)
            self._flow_process_execution = flow_process_execution
        else:
            self._flow_process_execution = None
        self._ensure_flow_program_pull_rpc_handler()
        self._start_idle_gc_loop_if_enabled()

    @property
    def runtime_loop(self) -> LoopThread:
        return self._runtime_loop

    @property
    def user_loop(self) -> LoopThread:
        return self._user_loop

    def submit_runtime_coro(self, coro: Coroutine) -> Future:
        self._ensure_open()
        return self._runtime_loop.submit(coro)

    def submit_user_coro(self, coro: Coroutine) -> Future:
        self._ensure_open()
        return self._user_loop.submit(coro)

    def call_local_actor(
        self,
        actor_id: str,
        method: str,
        *args: Any,
        timeout: float | None = None,
        **kwargs: Any,
    ) -> Any:
        self._ensure_open()
        fut = self.submit_user_coro(self.actor_api.call_local(actor_id, method, *args, **kwargs))
        return fut.result(timeout=timeout)

    def call_or_prepare_actor_call(
        self,
        target: Any,
        *args: Any,
        timeout: float | None = None,
        **kwargs: Any,
    ) -> dict[str, Any]:
        self._ensure_open()
        fut = self.submit_user_coro(self.actor_api.call_or_prepare_call(target, *args, **kwargs))
        return fut.result(timeout=timeout)

    def gc_idle_runtime_state(
        self,
        *,
        max_idle_seconds: float,
    ) -> dict[str, int]:
        self._ensure_open()
        return self.topic_api.gc_idle_runtime_state(
            max_idle_seconds=max_idle_seconds,
        )

    def shutdown(self, *, wait: bool = True, cancel_futures: bool = False) -> None:
        if self._closed:
            return
        self._closed = True
        self._stop_idle_gc_loop()
        callback_runtime = self._callback_runtime
        self._callback_runtime = None
        self.actor_api.set_callback_runtime(None)
        if callback_runtime is not None:
            callback_runtime.shutdown(
                wait=wait,
                cancel_futures=cancel_futures,
            )

        if self._owns_comm_hub:
            self.comm_hub.close()

        if self._owns_actor_api:
            self.actor_api.shutdown(wait=wait, cancel_futures=cancel_futures)
        elif self._owns_executor_lanes and self.executor_lanes is not None:
            self.executor_lanes.shutdown(wait=wait, cancel_futures=cancel_futures)

        if self._owns_runtime_loop:
            self._runtime_loop.stop(join=wait)
        if self._owns_user_loop:
            self._user_loop.stop(join=wait)

    def _ensure_open(self) -> None:
        if self._closed:
            raise RuntimeError("v1_runtime_host_closed")

    def _run_user_coro_blocking(self, coro: Coroutine[Any, Any, Any]) -> Any:
        fut = self.submit_user_coro(coro)
        return fut.result()

    def _ensure_actor_call_rpc_handler(self) -> None:
        if self.comm_hub.protocol_router.has_handler(
            plane=PLANE_RPC,
            op=OP_RPC_ACTOR_CALL,
        ):
            return
        register_actor_call_rpc_handler(
            actor_api=self.actor_api,
            comm_hub=self.comm_hub,
        )

    def _ensure_flow_program_pull_rpc_handler(self) -> None:
        if self.comm_hub.protocol_router.has_handler(
            plane=PLANE_RPC,
            op=OP_RPC_FLOW_PROGRAM_PULL,
        ):
            return
        resolve_flow_program = getattr(self.topic_api, "resolve_registered_flow_program", None)
        if not callable(resolve_flow_program):
            return
        register_flow_program_pull_handler(
            comm_hub=self.comm_hub,
            resolve_flow_program=(
                lambda flow_program_ref: resolve_flow_program(
                    flow_program_uri=flow_program_ref.program_uri,
                    flow_program_rev=flow_program_ref.program_rev,
                )
            ),
        )

    def _ensure_topic_forward_handlers(self) -> None:
        required_handlers = (
            (PLANE_DATA, OP_DATA_TOPIC_EVENT_FORWARD),
            (PLANE_CONTROL, OP_CONTROL_TOPIC_EVENT_CHAIN_DONE_FORWARD),
            (PLANE_CONTROL, OP_CONTROL_TOPIC_PRODUCER_DONE_FORWARD),
            (PLANE_CONTROL, OP_CONTROL_TOPIC_REQUEST_DONE_NOTIFY),
        )
        has_snapshot = [
            self.comm_hub.protocol_router.has_handler(plane=plane, op=op)
            for plane, op in required_handlers
        ]
        if all(has_snapshot):
            return
        if any(has_snapshot):
            raise RuntimeError("topic_forward_handler_registration_partial_state")
        register_topic_forward_handlers(
            topic_api=self.topic_api,
            comm_hub=self.comm_hub,
        )

    def _ensure_callback_runtime(self) -> None:
        if self._callback_runtime is not None:
            return
        callback_registry = CallbackRegistry(
            local_address=self.comm_hub.local_address,
            topic_api=self.topic_api,
            submit_user_coro=self.submit_user_coro,
            invoke_callback=self._invoke_actor_callback_target,
        )
        try:
            self.actor_api.resolve_local_actor(CALLBACK_REGISTRY_SERVICE_ACTOR_ID)
        except ValueError:
            self.actor_api.register_local_actor(
                CallbackRegistryServiceActor(callback_registry),
                actor_id=CALLBACK_REGISTRY_SERVICE_ACTOR_ID,
            )
        self._callback_runtime = ActorCallbackRuntime(
            local_address=self.comm_hub.local_address,
            actor_api=self.actor_api,
            comm_hub=self.comm_hub,
            callback_registry=callback_registry,
        )
        self.actor_api.set_callback_runtime(self._callback_runtime)

    async def _invoke_actor_callback_target(self, callback_target, event_record: dict[str, Any]) -> None:
        request_ref_id = str(
            event_record.get("event_group_id")
            or event_record.get("topic_uri")
            or "callback",
        )
        await call_actor_via_comm(
            self.actor_api,
            self.comm_hub,
            callback_target,
            dict(event_record),
            request_ref_id=request_ref_id,
        )

    async def _dispatch_actor_event(self, event) -> dict[str, Any]:
        publish_result = self.topic_api.publish_or_forward_event(
            topic_uri=event.topic_uri,
            event_group_id=event.event_group_id,
            payload=event.payload,
            tags=event.tags,
            seq=event.seq,
            epoch=None,
        )
        mode = str(publish_result.get("mode") or "").strip()
        if mode == "local":
            return {
                "delivery_mode": "local",
                "topic_uri": event.topic_uri,
                "event_group_id": event.event_group_id,
                "seq": event.seq,
            }
        await send_topic_forward_intent_via_comm(
            comm_hub=self.comm_hub,
            intent=publish_result,
        )
        return {
            "delivery_mode": "forwarded",
            "forward_mode": mode,
            "topic_uri": event.topic_uri,
            "event_group_id": event.event_group_id,
            "seq": event.seq,
        }

    def _start_idle_gc_loop_if_enabled(self) -> None:
        interval_seconds = self._gc_idle_interval_seconds
        if interval_seconds is None:
            return
        self._gc_loop_future = self.submit_runtime_coro(
            self._run_idle_gc_loop(
                interval_seconds=interval_seconds,
                ttl_seconds=self._gc_idle_ttl_seconds,
            )
        )

    def _stop_idle_gc_loop(self) -> None:
        gc_future = self._gc_loop_future
        self._gc_loop_future = None
        if gc_future is None:
            return
        gc_future.cancel()
        try:
            gc_future.result(timeout=0.5)
        except (FutureCancelledError, FutureTimeoutError):
            return
        except Exception:
            return

    async def _run_idle_gc_loop(
        self,
        *,
        interval_seconds: float,
        ttl_seconds: float,
    ) -> None:
        while not self._closed:
            await asyncio.sleep(interval_seconds)
            if self._closed:
                return
            try:
                self.topic_api.gc_idle_runtime_state(
                    max_idle_seconds=ttl_seconds,
                )
            except Exception:
                # GC failures must not break runtime service loops.
                continue

    @staticmethod
    def _normalize_optional_interval_seconds(
        raw_value: float | None,
        *,
        field_name: str,
    ) -> float | None:
        if raw_value is None:
            return None
        value = float(raw_value)
        if value <= 0.0:
            raise ValueError(f"{field_name} must be > 0 when provided.")
        return value

    @staticmethod
    def _normalize_non_negative_seconds(
        raw_value: float,
        *,
        field_name: str,
    ) -> float:
        value = float(raw_value)
        if value < 0.0:
            raise ValueError(f"{field_name} must be >= 0.")
        return value


__all__ = ["V1RuntimeHost"]
