# DOCS_REF:
# - docs/user/reference/env-vars.md
# - docs/user/01-quickstart-local.md
# - docs/user/02-quickstart-distributed.md
# DOCS_SYNC:
# - If runtime boot/watchdog env variable behavior changes, update env-vars
#   reference and quickstart guidance.

from __future__ import annotations
from pathlib import Path
from threading import Lock
from typing import Any, Optional
from urllib.parse import quote
import os
import time
import warnings
from flownet.config import FlownetConfig, RuntimeConfigInput, coerce_flownet_config
from flownet.core.request import FlowRequest, FlownetPacket
from flownet.core.stream_event import StreamEvent
from flownet.api.actor_method_ref import ActorMethodRef
from flownet.api.actor_ref import ActorRef
from flownet.core.actor import Actor
from flownet.runtime.transport.call_manager import RemoteCallManager
from flownet.runtime.loops import LoopLagMonitor, new_user_loop, submit_runtime_coro
from flownet.runtime.actors.registry import ActorRegistry
from flownet.engine.flow_engine import FlowEngine
from flownet.engine.adapters.runtime_adapter import build_engine_port
from flownet.runtime.actors.dispatcher import ActorDispatcher
from flownet.runtime.transport.runtime_protocol import RuntimeProtocol
from flownet.runtime.kernel import (
    RuntimeKernelBuilder,
    RuntimeTaskServices,
    RuntimeActorServices,
    RuntimeTopicServices,
    RuntimeSourceServices,
    RuntimeIOService,
    RuntimeStateServices,
    RuntimeFlowServices,
    RuntimeClusterServices,
    RuntimeNodeControlServices,
    register_kernel,
)
from flownet.runtime.cluster_view import build_naive_cluster_view
from flownet.runtime.cluster_gossip_protocol import (
    MSG_DELTA_ACK,
    MSG_DELTA_PULL,
    MSG_DELTA_PUSH,
    MSG_SNAPSHOT,
    make_delta_ack,
    make_snapshot,
    parse_envelope,
    validate_envelope,
)
from flownet.runtime.flows import FlowRunService, StreamRunTracker
from flownet.runtime.cluster_gossip_service import (
    ClusterGossipService,
    GossipServiceConfig,
)
from flownet.runtime.scheduler_hook import ClusterPlanResolver
from flownet.runtime.topics.flow_program_service import TopicFlowProgramService
from flownet.runtime.topics.topic_registry import TopicRegistry
from flownet.runtime.topics.topic_router import TopicRouter
from flownet.proactive.registry import ServiceRegistry
from flownet.runtime.sources.stream_sources import StreamSourceRegistry
from flownet.runtime.state import MemoryStateBackend, SQLiteStateBackend
from flownet.runtime.node_control import (
    NodeControlService,
    decode_node_control_response,
)


_runtime_instance = None
_runtime_instance_lock = Lock()
_runtime_config = FlownetConfig()
_GOSSIP_PULL_REPLY_MEMBERSHIP_LIMIT = 64


def _env_flag(name: str, default: bool = True) -> bool:
    value = os.environ.get(name)
    if value is None:
        return default
    return value.strip().lower() not in {"0", "false", "off", "no"}


def _env_float(name: str, default: float) -> float:
    value = os.environ.get(name)
    if value is None:
        return default
    try:
        return float(value)
    except ValueError:
        return default


def _user_loop_watchdog_enabled() -> bool:
    return _env_flag("FLOWNET_USER_LOOP_WATCHDOG", True)


def _user_loop_watch_interval() -> float:
    return _env_float("FLOWNET_USER_LOOP_WATCH_INTERVAL", 1.0)


def _user_loop_warn_threshold() -> float:
    return _env_float("FLOWNET_USER_LOOP_WARN_THRESHOLD", 2.0)


def _user_loop_warn_interval() -> float:
    return _env_float("FLOWNET_USER_LOOP_WARN_INTERVAL", 10.0)


def _cluster_metrics_log_interval() -> float:
    return _env_float("FLOWNET_CLUSTER_METRICS_LOG_INTERVAL", 30.0)


def _cluster_gossip_enabled() -> bool:
    return _env_flag("FLOWNET_CLUSTER_GOSSIP_ENABLED", True)


def _cluster_gossip_interval() -> float:
    return _env_float("FLOWNET_CLUSTER_GOSSIP_INTERVAL", 3.0)


def _cluster_gossip_snapshot_interval() -> float:
    return _env_float("FLOWNET_CLUSTER_GOSSIP_SNAPSHOT_INTERVAL", 30.0)


def _build_state_backend(config: FlownetConfig):
    backend_kind = str(getattr(config, "state_backend", "memory") or "memory").strip().lower()
    compatibility_version = int(getattr(config, "state_compatibility_version", 1))
    if backend_kind in {"memory", "mem"}:
        return MemoryStateBackend(compatibility_version=compatibility_version)

    if backend_kind in {"sqlite", "sqlite3"}:
        sqlite_path = getattr(config, "state_sqlite_path", None)
        if sqlite_path is None:
            base_dir = getattr(config, "state_dir", None) or config.local_ipc_dir
            sqlite_path = str(Path(base_dir).expanduser() / "flownet_state.sqlite3")
        try:
            return SQLiteStateBackend(
                sqlite_path,
                compatibility_version=compatibility_version,
            )
        except Exception as exc:  # pragma: no cover - fail-open fallback
            print(
                "[runtime] state backend init failed (fail-open fallback to memory): "
                f"backend={backend_kind}, error={exc}"
            )
            return MemoryStateBackend(compatibility_version=compatibility_version)

    print(
        "[runtime] unknown state backend kind (fail-open fallback to memory): "
        f"{backend_kind!r}"
    )
    return MemoryStateBackend(compatibility_version=compatibility_version)


def configure_runtime(config: RuntimeConfigInput):
    global _runtime_config
    _runtime_config = coerce_flownet_config(config)




def get_runtime():
    global _runtime_instance
    if _runtime_instance is None:
        from flownet.platform.user.startup import auto_boot_user_runtime
        auto_boot_user_runtime()
        with _runtime_instance_lock:
            if _runtime_instance is None:
                _runtime_instance = FlownetRuntime(config=_runtime_config)

    return _runtime_instance


def try_get_runtime():
    return _runtime_instance




class FlownetRuntime:

    def __init__(self, config:FlownetConfig):
        self._user_loop_thread = new_user_loop()
        self.user_loop = self._user_loop_thread.loop
        if _user_loop_watchdog_enabled():
            self._user_loop_monitor = LoopLagMonitor(
                self.user_loop,
                name="flownet-user-loop",
                interval=_user_loop_watch_interval(),
                warn_threshold=_user_loop_warn_threshold(),
                warn_interval=_user_loop_warn_interval(),
            )
        else:
            self._user_loop_monitor = None

        self._kernel_builder = RuntimeKernelBuilder()
        self.kernel = self._kernel_builder.kernel
        register_kernel(self.kernel)
        self.state_backend = _build_state_backend(config)

        self.protocol: RuntimeProtocol | None = None
        self.call_manager = RemoteCallManager(config=config, handler=self.message_handler)
        self._v1_comm_bridge_packet_type = "v1_envelope"
        self._v1_node_control_timeout_s = max(
            0.1,
            float(getattr(self.call_manager, "default_timeout", 5.0) or 5.0),
        )
        self._v1_comm_adapter: Any | None = None
        self._v1_comm_hub: Any | None = None
        self._init_v1_comm_bridge()
        local_endpoint_meta = self.call_manager.local_endpoint_metadata()
        # Cluster/scheduler boundary:
        # 1) Runtime owns ClusterView state materialization (env bootstrap + gossip apply).
        # 2) Scheduler consumes ClusterView to produce placement plans.
        # Keep this dependency one-way to avoid hidden coupling.
        self.cluster_view = build_naive_cluster_view(
            local_node_id=config.node_id,
            local_address=self.local_address(),
            local_schedulable=bool(getattr(config, "schedulable", True)),
            local_host_id=local_endpoint_meta.get("host_id"),
            local_uds_path=local_endpoint_meta.get("uds_path"),
            local_shm_path=local_endpoint_meta.get("shm_path"),
            local_transport_caps=local_endpoint_meta.get("transport_caps"),
        )
        self.call_manager.set_endpoint_lookup(self._lookup_endpoint_metadata)
        self.cluster_plan_resolver = ClusterPlanResolver(self.cluster_view)
        self.flow_run_service = FlowRunService()
        self.stream_run_tracker = StreamRunTracker()
        self._cluster_gossip_stats: dict[str, int] = {
            "received_total": 0,
            MSG_DELTA_PUSH: 0,
            MSG_DELTA_PULL: 0,
            MSG_DELTA_ACK: 0,
            MSG_SNAPSHOT: 0,
            "membership_entries": 0,
            "pull_served_total": 0,
            "ack_served_total": 0,
            "snapshot_served_total": 0,
        }
        self.cluster_gossip_service = ClusterGossipService(
            cluster_view=self.cluster_view,
            local_address_fn=self.local_address,
            send_packet_fn=self.call_manager.send_packet,
            config=GossipServiceConfig(
                enabled=_cluster_gossip_enabled(),
                interval_s=_cluster_gossip_interval(),
                snapshot_interval_s=_cluster_gossip_snapshot_interval(),
            ),
        )
        self.topic_router = TopicRouter(
            cluster_view=self.cluster_view,
            local_address_fn=self.local_address,
        )
        self.topic_registry = TopicRegistry(
            call_manager=self.call_manager,
            local_address_fn=self.local_address,
            submit_coro=submit_runtime_coro,
            submit_user_coro=self._user_loop_thread.submit,
            handle_flow_request=self.handle_flow_request,
            topic_router=self.topic_router,
            track_request_delta=self._track_topic_request_delta,
            track_request_producer_done=self._mark_topic_request_producer_done,
            send_topic_event_via_v1=self._forward_topic_event_via_v1,
            v1_fail_open_legacy=True,
        )
        self.topic_flow_program_service = TopicFlowProgramService(
            topic_registry=self.topic_registry,
            local_address_fn=self.local_address,
            submit_flow_fn=self.flow_run_service.submit_flow,
        )
        self.source_registry = StreamSourceRegistry(
            call_manager=self.call_manager,
            topic_registry=self.topic_registry,
            local_address_fn=self.local_address,
            submit_coro=submit_runtime_coro,
            submit_user_coro=self._user_loop_thread.submit,
            request_id_factory=self.stream_run_tracker.new_request_id,
            mark_done_fn=self.stream_run_tracker.mark_producer_done,
        )
        self.service_registry = ServiceRegistry(
            call_manager=self.call_manager,
            local_address_fn=self.local_address,
            submit_coro=submit_runtime_coro,
            submit_user_coro=self._user_loop_thread.submit,
            topic_registry=self.topic_registry,
        )
        self.node_control_service = NodeControlService(
            runtime=self,
            service_registry=self.service_registry,
        )
        self.actor_registry = ActorRegistry(self.local_address)
        self.actor_dispatcher = ActorDispatcher(
            call_manager=self.call_manager,
            local_address_fn=self.local_address,
            resolve_actor_fn=self.actor_registry.resolve,
            user_loop=self.user_loop,
            container_handler=self,
            call_remote_via_v1=self._call_actor_via_v1,
            v1_fail_open_legacy=True,
        )
        self._kernel_builder.register_tasks(
            RuntimeTaskServices(
                submit_runtime_coro=submit_runtime_coro,
                submit_user_coro=self._user_loop_thread.submit,
            )
        )
        self._kernel_builder.register_io(RuntimeIOService(call_manager=self.call_manager))
        self._kernel_builder.register_state(
            RuntimeStateServices(
                backend=self.state_backend,
            )
        )
        self._kernel_builder.register_topics(
            RuntimeTopicServices(
                service_registry=self.service_registry,
                topic_registry=self.topic_registry,
            )
        )
        self._kernel_builder.register_sources(
            RuntimeSourceServices(
                registry=self.source_registry,
            )
        )
        self._kernel_builder.register_cluster(
            # Cluster service intentionally exposes both "view" and "resolve_plan"
            # as separate facets to preserve cluster-state vs scheduler-policy
            # logical decoupling, even when running in one process.
            RuntimeClusterServices(
                view=self.cluster_view,
                resolve_plan_fn=self.cluster_plan_resolver.resolve_plan,
                metrics_snapshot_fn=self.cluster_metrics_snapshot,
                reset_metrics_fn=self.reset_cluster_metrics,
                gossip_stats_fn=self.cluster_gossip_stats,
            )
        )
        self._kernel_builder.register_node_control(
            RuntimeNodeControlServices(
                service=self.node_control_service,
                call_manager=self.call_manager,
                local_address_fn=self.local_address,
                call_via_v1_fn=self._call_node_control_via_v1,
                v1_fail_open_legacy=True,
            )
        )
        self._register_v1_node_control_handler()
        self._register_v1_actor_call_handler()
        self._register_v1_topic_event_forward_handler()
        self._kernel_builder.register_actors(
            RuntimeActorServices(
                dispatcher=self.actor_dispatcher,
                local_address_fn=self.local_address,
                resolve_actor_fn=self.actor_registry.resolve,
                create_actor_fn=self.actor_registry.create_actor,
                make_object_actor_fn=self.actor_registry.make_object_actor,
                register_actor_fn=self.actor_registry.register_actor,
                lookup_actor_fn=self.actor_registry.lookup_actor,
            )
        )

        port = build_engine_port(self.kernel)
        self.flow_engine = FlowEngine(port=port)
        self._kernel_builder.register_flows(
            RuntimeFlowServices(
                handle_flow_request=self.handle_flow_request,
                submit_runtime_coro=submit_runtime_coro,
                submit_flow_fn=self.flow_run_service.submit_flow,
                lookup_run_fn=self.flow_run_service.lookup_run,
                register_flow_program_fn=self.topic_flow_program_service.register_flow_program,
                resolve_flow_program_payload_fn=self.topic_flow_program_service.resolve_flow_program_payload,
                list_flow_programs_fn=self.topic_flow_program_service.list_flow_programs,
                get_flow_program_fn=self.topic_flow_program_service.get_flow_program,
                delete_flow_program_fn=self.topic_flow_program_service.delete_flow_program,
                submit_flow_program_fn=self.topic_flow_program_service.submit_flow_program,
                stream_tracker_stats_fn=self.stream_run_tracker.stats,
                stream_tracker_request_status_fn=self.stream_run_tracker.request_status_snapshot,
            )
        )
        self.protocol = RuntimeProtocol(
            call_manager=self.call_manager,
            flow_engine=self.flow_engine,
            actor_dispatcher=self.actor_dispatcher,
            service_registry=self.service_registry,
            topic_registry=self.topic_registry,
            cluster_gossip_handler=self.handle_cluster_gossip_packet,
            node_control_handler=self.kernel.require_node_control().handle_packet,
            v1_envelope_handler=self._handle_v1_envelope_packet,
            cluster_metrics_snapshot=self.cluster_metrics_snapshot,
            metrics_log_interval_s=_cluster_metrics_log_interval(),
        )
        self.cluster_gossip_service.start(submit_runtime_coro)
        self.config = config



    async def message_handler(self, packet:FlownetPacket) -> FlownetPacket | None:
        if self.protocol is None:
            raise RuntimeError("Runtime protocol not initialized.")
        return await self.protocol.handle_packet(packet)

    def ping(self):
        return "pong"

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

    def subscribe_service_events(self, service_name: str, address: str) -> None:
        warnings.warn(
            "FlownetRuntime.subscribe_service_events() is deprecated and will be removed. "
            "Use ServiceRegistry.subscribe_flow(service_name, flow_task, execution_address, tags).",
            DeprecationWarning,
            stacklevel=2,
        )
        subscribe = getattr(self.service_registry, "subscribe", None)
        if callable(subscribe):
            subscribe(service_name, address)
            return
        raise RuntimeError(
            "ServiceRegistry does not implement subscribe(). "
            "Use subscribe_flow(service_name, flow_task, execution_address, tags)."
        )

    def register_service_event_handler(self, service_name: str, handler):
        warnings.warn(
            "FlownetRuntime.register_service_event_handler() is deprecated and will be removed. "
            "Use ServiceRegistry.subscribe_flow(service_name, flow_task, execution_address, tags).",
            DeprecationWarning,
            stacklevel=2,
        )
        register_handler = getattr(self.service_registry, "register_event_handler", None)
        if callable(register_handler):
            register_handler(service_name, handler)
            return
        raise RuntimeError(
            "ServiceRegistry does not implement register_event_handler(). "
            "Use subscribe_flow(service_name, flow_task, execution_address, tags)."
        )
        
        
        
        
        
    def resolve(self, actor_id: str) -> Actor:
        return self.actor_registry.resolve(actor_id)

    async def create_actor(
        self, actor_class: type, *args, actor_config: Any | None = None, **kwargs
    ) -> ActorRef:
        return await self.actor_registry.create_actor(
            actor_class, *args, actor_config=actor_config, **kwargs
        )
    
    def make_object_actor(
        self, actor_object: Any, actor_config: Any | None = None
    ) -> ActorRef:
        return self.actor_registry.make_object_actor(
            actor_object, actor_config=actor_config
        )

    def register_actor(
        self, actor_id: str, actor_object: Any, actor_config: Any | None = None
    ) -> ActorRef:
        return self.actor_registry.register_actor(
            actor_id, actor_object, actor_config=actor_config
        )


    def lookup_actor(self, actor_id: str) -> Optional[Actor]:
        return self.actor_registry.lookup_actor(actor_id)

    def register_flow_program(
        self,
        flow_def_or_task,
        *,
        flow_uri: str,
        version: str,
        metadata: dict[str, Any] | None = None,
        io_contract: dict[str, Any] | None = None,
    ) -> dict[str, Any]:
        return self.topic_flow_program_service.register_flow_program(
            flow_def_or_task,
            flow_uri=flow_uri,
            version=version,
            metadata=metadata,
            io_contract=io_contract,
        )

    def list_flow_programs(
        self,
        *,
        prefix: str | None = None,
        tags: dict[str, Any] | None = None,
        limit: int | None = None,
    ) -> list[dict[str, Any]]:
        return self.topic_flow_program_service.list_flow_programs(
            prefix=prefix,
            tags=tags,
            limit=limit,
        )

    def get_flow_program(self, flow_uri: str, version: str | None = None) -> dict[str, Any] | None:
        return self.topic_flow_program_service.get_flow_program(flow_uri, version=version)

    def resolve_flow_program_payload(self, flow_uri: str, version: str) -> Any:
        return self.topic_flow_program_service.resolve_flow_program_payload(flow_uri, version=version)

    def delete_flow_program(self, flow_uri: str, version: str | None = None) -> dict[str, Any]:
        return self.topic_flow_program_service.delete_flow_program(flow_uri, version=version)

    def submit_flow_program(
        self,
        flow_uri: str,
        version: str | None = None,
        *,
        bindings: dict[str, Any] | None = None,
        ingress=None,
        egress=None,
        run_config: dict[str, Any] | None = None,
    ):
        return self.topic_flow_program_service.submit_flow_program(
            flow_uri,
            version=version,
            bindings=bindings,
            ingress=ingress,
            egress=egress,
            run_config=run_config,
        )

    def get_stateless_op_blob(self, op_id: str) -> bytes | None:
        from flownet.runtime.stateless_ops import get_serialized_op
        return get_serialized_op(op_id)

    def _init_v1_comm_bridge(self) -> None:
        try:
            from flownet.v1.runtime.comm import V0TransportHubAdapter, V1CommHub
        except Exception as exc:  # pragma: no cover - defensive compatibility path
            warnings.warn(
                f"v1 comm bridge import failed; node-control bridge disabled: {exc}",
                RuntimeWarning,
                stacklevel=2,
            )
            self._v1_comm_adapter = None
            self._v1_comm_hub = None
            return

        self._v1_comm_adapter = V0TransportHubAdapter(
            local_address=self.local_address(),
            transport_hub=self.call_manager.transport_hub,
            packet_type=self._v1_comm_bridge_packet_type,
        )
        self._v1_comm_hub = V1CommHub(
            local_address=self.local_address(),
            transport=self._v1_comm_adapter,
        )

    def _register_v1_node_control_handler(self) -> None:
        hub = self._v1_comm_hub
        if hub is None:
            return
        from flownet.v1.runtime.comm import OP_CONTROL_NODE_CALL, PLANE_CONTROL

        router = hub.protocol_router
        if router.has_handler(plane=PLANE_CONTROL, op=OP_CONTROL_NODE_CALL):
            return

        async def _handle(envelope) -> dict[str, Any]:
            payload = envelope.body if isinstance(getattr(envelope, "body", None), dict) else {}
            return await self.kernel.require_node_control().handle_packet(payload)

        router.register_handler(
            plane=PLANE_CONTROL,
            op=OP_CONTROL_NODE_CALL,
            handler=_handle,
        )

    def _register_v1_actor_call_handler(self) -> None:
        hub = self._v1_comm_hub
        if hub is None:
            return
        from flownet.v1.runtime.comm import OP_RPC_ACTOR_CALL, PLANE_RPC

        router = hub.protocol_router
        if router.has_handler(plane=PLANE_RPC, op=OP_RPC_ACTOR_CALL):
            return

        async def _handle(envelope) -> dict[str, Any]:
            body = envelope.body if isinstance(getattr(envelope, "body", None), dict) else {}
            mode = str(body.get("mode") or "").strip()
            if mode != "actor_call_intent":
                raise ValueError(f"unsupported_actor_call_intent_mode:{mode}")

            address = str(body.get("address") or "").strip()
            if not address:
                raise ValueError("actor_call_intent.address must be non-empty.")
            local_address = self.local_address()
            if address != local_address:
                raise RuntimeError(
                    "call_intent_owner_mismatch:"
                    f" expected_owner={address},"
                    f" local_address={local_address}",
                )

            actor_id = str(body.get("actor_id") or "").strip()
            if not actor_id:
                raise ValueError("actor_call_intent.actor_id must be non-empty.")
            method = str(body.get("method") or "").strip()
            if not method:
                raise ValueError("actor_call_intent.method must be non-empty.")
            raw_args = body.get("args")
            if raw_args is None:
                args: tuple[Any, ...] = ()
            elif isinstance(raw_args, (list, tuple)):
                args = tuple(raw_args)
            else:
                raise TypeError("actor_call_intent.args must be a list/tuple when provided.")
            raw_kwargs = body.get("kwargs")
            if raw_kwargs is None:
                kwargs: dict[str, Any] = {}
            elif isinstance(raw_kwargs, dict):
                kwargs = dict(raw_kwargs)
            else:
                raise TypeError("actor_call_intent.kwargs must be a dict when provided.")

            rmf = ActorMethodRef(
                address=local_address,
                actor_id=actor_id,
                method=method,
            )
            try:
                result = await self.actor_dispatcher.handle_actor_call(
                    rmf,
                    *args,
                    **kwargs,
                )
            except Exception as exc:
                return {
                    "mode": "applied_actor_call_error",
                    "address": local_address,
                    "actor_id": actor_id,
                    "method": method,
                    "error_type": exc.__class__.__name__,
                    "message": str(exc),
                }
            return {
                "mode": "applied_actor_call",
                "address": local_address,
                "actor_id": actor_id,
                "method": method,
                "result": result,
            }

        router.register_handler(
            plane=PLANE_RPC,
            op=OP_RPC_ACTOR_CALL,
            handler=_handle,
        )

    def _register_v1_topic_event_forward_handler(self) -> None:
        hub = self._v1_comm_hub
        if hub is None:
            return
        from flownet.v1.runtime.comm import OP_DATA_TOPIC_EVENT_FORWARD, PLANE_DATA

        router = hub.protocol_router
        if router.has_handler(plane=PLANE_DATA, op=OP_DATA_TOPIC_EVENT_FORWARD):
            return

        def _normalize_bool(value: Any) -> bool:
            if isinstance(value, bool):
                return value
            if isinstance(value, str):
                normalized = value.strip().lower()
                if normalized in {"1", "true", "yes", "on"}:
                    return True
                if normalized in {"0", "false", "no", "off"}:
                    return False
            return bool(value)

        async def _handle(envelope) -> dict[str, Any]:
            body = envelope.body if isinstance(getattr(envelope, "body", None), dict) else {}
            mode = str(body.get("mode") or "").strip()
            if mode != "forward_topic_event":
                raise ValueError(f"unsupported_topic_forward_mode:{mode}")

            topic_uri = str(body.get("topic_uri") or "").strip()
            if not topic_uri:
                raise ValueError("forward_topic_event.topic_uri must be non-empty.")
            event_group_id = str(body.get("event_group_id") or "").strip()
            if not event_group_id:
                raise ValueError("forward_topic_event.event_group_id must be non-empty.")
            event_type = str(body.get("event_type") or "").strip() or "event"
            raw_tags = body.get("tags")
            tags = dict(raw_tags) if isinstance(raw_tags, dict) else {}
            tags.setdefault("request_ref_id", event_group_id)
            raw_seq = body.get("seq")
            seq = self._coerce_non_negative_int(raw_seq)
            if seq is not None:
                tags.setdefault("__seq__", str(seq))
            producer_id = (
                str(body.get("producer_id")).strip()
                if body.get("producer_id") is not None
                else None
            )
            if producer_id == "":
                producer_id = None
            allow_reserved_namespace = _normalize_bool(body.get("allow_reserved_namespace", False))
            origin = str(body.get("origin") or "").strip() or getattr(envelope, "source_address", None)

            self.topic_registry.publish(
                topic_uri,
                event_type,
                body.get("payload"),
                tags,
                producer_id=producer_id,
                allow_reserved_namespace=allow_reserved_namespace,
                origin=origin,
            )
            return {
                "mode": "applied_topic_event_forward",
                "topic_uri": topic_uri,
                "event_group_id": event_group_id,
                "event_type": event_type,
            }

        router.register_handler(
            plane=PLANE_DATA,
            op=OP_DATA_TOPIC_EVENT_FORWARD,
            handler=_handle,
        )

    async def _call_node_control_via_v1(
        self,
        target_address: str,
        op: str,
        args: tuple[Any, ...],
        kwargs: dict[str, Any],
    ) -> Any:
        hub = self._v1_comm_hub
        if hub is None:
            raise RuntimeError("v1_node_control_bridge_not_configured")
        from flownet.v1.runtime.comm import OP_CONTROL_NODE_CALL, PLANE_CONTROL

        reply = await hub.request(
            plane=PLANE_CONTROL,
            op=OP_CONTROL_NODE_CALL,
            target_address=target_address,
            timeout=self._v1_node_control_timeout_s,
            request_ref_id=f"node_control:{op}",
            body={
                "reply_to": self.local_address(),
                "op": op,
                "args": list(args),
                "kwargs": dict(kwargs),
            },
        )
        return decode_node_control_response(reply.body, op=op, address=target_address)

    async def _forward_topic_event_via_v1(
        self,
        route: TopicRoute,
        event: StreamEvent,
        producer_id: str | None,
        allow_reserved_namespace: bool,
    ) -> None:
        hub = self._v1_comm_hub
        if hub is None:
            raise RuntimeError("v1_topic_bridge_not_configured")
        from flownet.v1.runtime.topics import send_topic_forward_intent_via_comm

        event_group_id = (
            str(event.request_ref_id).strip()
            if event.request_ref_id is not None
            else ""
        )
        if not event_group_id:
            event_group_id = uuid.uuid4().hex
        seq = self._coerce_non_negative_int((event.tags or {}).get("__seq__"))

        intent = {
            "mode": "forward_topic_event",
            "coordinator_address": route.coordinator,
            "topic_uri": route.topic_id,
            # v0 route view has no epoch; bridge currently uses baseline epoch lane.
            "epoch": 0,
            "event_group_id": event_group_id,
            "payload": event.payload,
            "tags": dict(event.tags or {}),
            "seq": seq,
            # v0-only fields consumed by runtime-side v1 topic bridge handler.
            "event_type": str(event.event_type or "").strip() or "event",
            "origin": str(event.origin or "").strip() or self.local_address(),
            "producer_id": producer_id,
            "allow_reserved_namespace": bool(allow_reserved_namespace),
        }
        await send_topic_forward_intent_via_comm(
            comm_hub=hub,
            intent=intent,
        )

    async def _call_actor_via_v1(
        self,
        rmf: ActorMethodRef,
        args: tuple[Any, ...],
        kwargs: dict[str, Any],
    ) -> Any:
        hub = self._v1_comm_hub
        if hub is None:
            raise RuntimeError("v1_actor_bridge_not_configured")
        from flownet.v1.runtime.comm import OP_RPC_ACTOR_CALL, PLANE_RPC

        target_address = str(getattr(rmf, "address", "") or "").strip()
        if not target_address:
            raise ValueError("ActorMethodRef.address must be non-empty.")
        actor_id = str(getattr(rmf, "actor_id", "") or "").strip()
        if not actor_id:
            raise ValueError("ActorMethodRef.actor_id must be non-empty.")
        method = str(getattr(rmf, "method", "") or "").strip()
        if not method:
            raise ValueError("ActorMethodRef.method must be non-empty.")

        reply = await hub.request(
            plane=PLANE_RPC,
            op=OP_RPC_ACTOR_CALL,
            target_address=target_address,
            timeout=self._v1_node_control_timeout_s,
            request_ref_id=f"actor_call:{actor_id}",
            body={
                "mode": "actor_call_intent",
                "address": target_address,
                "actor_id": actor_id,
                "method": method,
                "args": list(args),
                "kwargs": dict(kwargs),
            },
        )
        body = reply.body if isinstance(getattr(reply, "body", None), dict) else {}
        mode = str(body.get("mode") or "").strip()
        if mode == "applied_actor_call":
            return body.get("result")
        if mode == "applied_actor_call_error":
            error_type = str(body.get("error_type") or "RuntimeError")
            message = str(body.get("message") or "remote_actor_call_failed")
            raise RuntimeError(
                f"[FlownetRuntime] Remote actor call failed "
                f"[address={target_address}, actor_id={actor_id}, method={method}, type={error_type}]: {message}"
            )
        raise RuntimeError(f"unsupported_actor_call_reply_mode:{mode}")

    async def _handle_v1_envelope_packet(self, packet: FlownetPacket) -> None:
        adapter = self._v1_comm_adapter
        if adapter is None:
            return None
        await adapter.receive_packet(packet)
        return None

    @staticmethod
    def _coerce_non_negative_int(value: Any) -> int | None:
        if value is None:
            return None
        try:
            parsed = int(value)
        except (TypeError, ValueError):
            return None
        if parsed < 0:
            return None
        return parsed

    def local_address(self) -> str:
        return f"{self.call_manager.network_interface.host}:{self.call_manager.network_interface.port}"

    async def handle_flow_request(self, request: FlowRequest):
        return await self.flow_engine.handle_flow_request(request)

    async def _track_topic_request_delta(
        self,
        origin_address: str,
        request_id: str,
        delta: int,
    ) -> None:
        if not request_id:
            return
        target_address = str(origin_address or "").strip()
        if not target_address:
            return
        if target_address == self.local_address():
            self.stream_run_tracker.event_chain_done(request_id, delta)
            return
        control = self.kernel.require_node_control()
        await control.call_on(
            target_address,
            "stream_tracker_event_chain_done",
            request_id,
            delta,
        )

    async def _mark_topic_request_producer_done(
        self,
        origin_address: str,
        request_id: str,
    ) -> None:
        if not request_id:
            return
        target_address = str(origin_address or "").strip()
        if not target_address:
            return
        if target_address == self.local_address():
            self.stream_run_tracker.mark_producer_done(request_id)
            return
        await self.kernel.require_node_control().call_on(
            target_address,
            "stream_tracker_request_producer_done",
            request_id,
        )

    def event_chain_request_done(
        self,
        coordinator_target: str,
        request_id: str,
        delta: int,
    ) -> int:
        normalized_target = str(coordinator_target or "").strip()
        normalized_request_id = str(request_id or "").strip()
        if not normalized_target or not normalized_request_id:
            return 0
        try:
            normalized_delta = int(delta)
        except (TypeError, ValueError):
            return 0

        actor = self.lookup_actor(normalized_target)
        if actor is None:
            return 0
        event_chain_done = getattr(actor.object, "event_chain_done", None)
        if not callable(event_chain_done):
            return 0
        result = event_chain_done(normalized_request_id, normalized_delta)
        try:
            return int(result)
        except (TypeError, ValueError):
            return 0

    def handle_cluster_gossip_packet(self, msg_type: str, payload: Any) -> dict[str, Any]:
        envelope = parse_envelope(payload, msg_type=msg_type)
        validate_envelope(envelope)
        applied = None
        gossip_reply = None
        if envelope.delta is not None and msg_type in {MSG_DELTA_PUSH, MSG_SNAPSHOT}:
            applied = self.cluster_view.apply_gossip_delta(
                membership=envelope.delta.membership,
                tombstones=envelope.delta.tombstones,
                delta_ts_ms=envelope.sent_at_ms,
                source_node=envelope.from_node,
                source_delta_seq=envelope.delta_seq,
            )
        if msg_type == MSG_DELTA_PULL:
            now_ms = int(time.time() * 1000.0)
            ack = make_delta_ack(
                from_node=self.cluster_view.local_node_id(),
                to_node=envelope.from_node,
                view_epoch=self.cluster_view.view_epoch(),
                delta_seq=envelope.delta_seq,
                sent_at_ms=now_ms,
            ).as_dict()
            snapshot = make_snapshot(
                from_node=self.cluster_view.local_node_id(),
                to_node=envelope.from_node,
                view_epoch=self.cluster_view.view_epoch(),
                delta_seq=envelope.delta_seq,
                sent_at_ms=now_ms,
                membership=self._gossip_membership_payload(
                    limit=_GOSSIP_PULL_REPLY_MEMBERSHIP_LIMIT
                ),
            ).as_dict()
            gossip_reply = {"ack": ack, "snapshot": snapshot}
            self._cluster_gossip_stats["pull_served_total"] = (
                self._cluster_gossip_stats.get("pull_served_total", 0) + 1
            )
            self._cluster_gossip_stats["ack_served_total"] = (
                self._cluster_gossip_stats.get("ack_served_total", 0) + 1
            )
            self._cluster_gossip_stats["snapshot_served_total"] = (
                self._cluster_gossip_stats.get("snapshot_served_total", 0) + 1
            )
        self._cluster_gossip_stats["received_total"] = (
            self._cluster_gossip_stats.get("received_total", 0) + 1
        )
        self._cluster_gossip_stats[msg_type] = self._cluster_gossip_stats.get(msg_type, 0) + 1
        if envelope.delta is not None:
            self._cluster_gossip_stats["membership_entries"] = (
                self._cluster_gossip_stats.get("membership_entries", 0)
                + len(envelope.delta.membership)
            )
        return {
            "ok": True,
            "msg_type": msg_type,
            "view_epoch": envelope.view_epoch,
            "delta_seq": envelope.delta_seq,
            "stats": dict(self._cluster_gossip_stats),
            "cluster_view_epoch": self.cluster_view.view_epoch(),
            "applied": applied,
            "gossip_reply": gossip_reply,
        }

    def _gossip_membership_payload(self, *, limit: int | None = None) -> dict[str, dict[str, Any]]:
        nodes = sorted(self.cluster_view.list_nodes(), key=lambda node: node.node_id)
        if isinstance(limit, int) and limit >= 0:
            nodes = nodes[:limit]
        payload: dict[str, dict[str, Any]] = {}
        for node in nodes:
            data = node.as_dict()
            data.pop("node_id", None)
            payload[node.node_id] = data
        return payload

    def _lookup_endpoint_metadata(self, address: str) -> dict[str, object] | None:
        node = self.cluster_view.find_node_by_address(address)
        if node is None:
            return None
        result: dict[str, object] = {
            "address": node.address,
            "host_id": node.host_id,
            "uds_path": node.uds_path,
            "shm_path": node.shm_path,
        }
        if node.transport_caps:
            result["transport_caps"] = list(node.transport_caps)
        return result

    def cluster_metrics_snapshot(self) -> dict[str, Any]:
        snapshot = dict(self.cluster_plan_resolver.metrics_snapshot())
        snapshot["stream_tracker"] = self.stream_run_tracker.stats()
        flow_engine = getattr(self, "flow_engine", None)
        stats_fn = getattr(flow_engine, "operator_stats_snapshot", None)
        snapshot["operator_stats"] = stats_fn() if callable(stats_fn) else {}
        call_manager = getattr(self, "call_manager", None)
        transport_stats_fn = getattr(call_manager, "transport_metrics_snapshot", None)
        snapshot["transport"] = transport_stats_fn() if callable(transport_stats_fn) else {}
        return snapshot

    def reset_cluster_metrics(self) -> None:
        self.cluster_plan_resolver.reset_metrics()

    def cluster_gossip_stats(self) -> dict[str, Any]:
        return {
            "received": dict(self._cluster_gossip_stats),
            "sender": self.cluster_gossip_service.stats_snapshot(),
        }

    def list_runtime_topics(self) -> list[dict[str, Any]]:
        topic_registry = getattr(self, "topic_registry", None)
        if topic_registry is None:
            return []
        list_topics = getattr(topic_registry, "list_topics", None)
        if callable(list_topics):
            records: list[dict[str, Any]] = []
            for item in list_topics() or []:
                if not isinstance(item, dict):
                    continue
                topic_id = str(item.get("topic_id") or "").strip()
                if not topic_id:
                    continue
                metadata = dict(item.get("metadata") or {})
                metadata["execution_addresses"] = sorted(
                    str(address).strip()
                    for address in (metadata.get("execution_addresses") or [])
                    if str(address).strip()
                )
                metadata["subscription_ids"] = sorted(
                    str(subscription_id).strip()
                    for subscription_id in (metadata.get("subscription_ids") or [])
                    if str(subscription_id).strip()
                )
                records.append(
                    {
                        "topic_id": topic_id,
                        "uri": str(item.get("uri") or _topic_uri(topic_id)),
                        "subscription_count": int(item.get("subscription_count", 0)),
                        "source": str(item.get("source") or "runtime"),
                        "metadata": metadata,
                    }
                )
            records.sort(key=lambda item: str(item.get("topic_id") or ""))
            return records

        list_subscriptions = getattr(topic_registry, "list_subscriptions", None)
        if not callable(list_subscriptions):
            return []

        topics: dict[str, dict[str, Any]] = {}
        for subscription in list_subscriptions() or []:
            topic_id = str(subscription.get("topic_id") or "").strip()
            if not topic_id:
                continue
            row = topics.setdefault(
                topic_id,
                {
                    "topic_id": topic_id,
                    "uri": _topic_uri(topic_id),
                    "subscription_count": 0,
                    "source": "runtime",
                    "metadata": {
                        "execution_addresses": set(),
                        "subscription_ids": set(),
                    },
                },
            )
            row["subscription_count"] = int(row.get("subscription_count", 0)) + 1
            metadata = row["metadata"]
            execution_address = str(subscription.get("execution_address") or "").strip()
            if execution_address:
                metadata["execution_addresses"].add(execution_address)
            subscription_id = str(subscription.get("subscription_id") or "").strip()
            if subscription_id:
                metadata["subscription_ids"].add(subscription_id)

        records: list[dict[str, Any]] = []
        for row in topics.values():
            metadata = dict(row.get("metadata") or {})
            metadata["execution_addresses"] = sorted(metadata.get("execution_addresses") or [])
            metadata["subscription_ids"] = sorted(metadata.get("subscription_ids") or [])
            records.append(
                {
                    "topic_id": row["topic_id"],
                    "uri": row["uri"],
                    "subscription_count": int(row.get("subscription_count", 0)),
                    "source": str(row.get("source") or "runtime"),
                    "metadata": metadata,
                }
            )
        records.sort(key=lambda item: str(item.get("topic_id") or ""))
        return records

    def list_runtime_actors(self) -> list[dict[str, Any]]:
        actor_registry = getattr(self, "actor_registry", None)
        if actor_registry is None:
            return []
        list_actors = getattr(actor_registry, "list_actors", None)
        if not callable(list_actors):
            return []

        address = self.local_address()
        records: list[dict[str, Any]] = []
        for item in list_actors() or []:
            actor_id = str(item.get("actor_id") or "").strip()
            if not actor_id:
                continue
            records.append(
                {
                    "actor_id": actor_id,
                    "uri": _actor_uri(actor_id),
                    "address": address,
                    "class_name": str(item.get("class_name") or "unknown"),
                    "module": str(item.get("module") or "unknown"),
                    "source": "runtime",
                    "metadata": {},
                }
            )
        records.sort(key=lambda item: str(item.get("actor_id") or ""))
        return records

    def list_runtime_nodes(self) -> list[dict[str, Any]]:
        cluster_view = getattr(self, "cluster_view", None)
        if cluster_view is None:
            return []
        list_nodes = getattr(cluster_view, "list_nodes", None)
        if not callable(list_nodes):
            return []

        local_node_id_fn = getattr(cluster_view, "local_node_id", None)
        locality_tier_fn = getattr(cluster_view, "locality_tier", None)
        is_node_schedulable_fn = getattr(cluster_view, "is_node_schedulable", None)

        local_node_id = None
        if callable(local_node_id_fn):
            try:
                local_node_id = str(local_node_id_fn())
            except Exception:
                local_node_id = None

        records: list[dict[str, Any]] = []
        for node in list_nodes() or []:
            node_id = str(getattr(node, "node_id", "") or "").strip()
            if not node_id:
                continue
            address = str(getattr(node, "address", "") or "").strip() or "unknown"
            health = str(getattr(node, "health", "unknown") or "unknown")
            locality = "local" if local_node_id is not None and node_id == local_node_id else "remote"
            if callable(locality_tier_fn):
                try:
                    locality = str(locality_tier_fn(node_id))
                except Exception:
                    pass
            schedulable = False
            if callable(is_node_schedulable_fn):
                try:
                    schedulable = bool(is_node_schedulable_fn(node_id))
                except Exception:
                    schedulable = False

            records.append(
                {
                    "node_id": node_id,
                    "uri": _node_uri(node_id),
                    "address": address,
                    "health": health,
                    "locality": locality,
                    "schedulable": schedulable,
                    "source": "runtime",
                    "metadata": {
                        "zone": getattr(node, "zone", None),
                        "region": getattr(node, "region", None),
                        "last_seen": getattr(node, "last_seen", None),
                        "resource_summary": dict(getattr(node, "resource_summary", None) or {}),
                        "host_id": getattr(node, "host_id", None),
                        "uds_path": getattr(node, "uds_path", None),
                        "shm_path": getattr(node, "shm_path", None),
                        "transport_caps": list(getattr(node, "transport_caps", None) or []),
                    },
                }
            )
        records.sort(key=lambda item: str(item.get("node_id") or ""))
        return records


def _topic_uri(topic_id: str) -> str:
    return f"topic://{quote(topic_id, safe='')}"


def _actor_uri(actor_id: str) -> str:
    return f"actor://{quote(actor_id, safe='')}"


def _node_uri(node_id: str) -> str:
    return f"node://{quote(node_id, safe='')}"
