from __future__ import annotations

import asyncio
import os
from concurrent.futures import TimeoutError as FutureTimeoutError
from typing import Any

from flownet.api.actor_method_ref import ActorMethodRef
from flownet.runtime.contexts.actor import _system_calls_scope
from flownet.runtime import loops
from flownet.runtime.kernel import get_runtime_kernel


def _runtime_loop():
    return loops.get_runtime_loop()


def _in_runtime_loop() -> bool:
    try:
        loop = asyncio.get_running_loop()
    except RuntimeError:
        return False
    return loop is _runtime_loop()


def submit_runtime_coro(coro):
    return loops.submit_runtime_coro(coro)


def _resolve_sync_wait_timeout_seconds() -> float | None:
    explicit = (os.environ.get("FLOWNET_SYNC_CALL_TIMEOUT_SECONDS") or "").strip()
    if explicit:
        try:
            parsed = float(explicit)
            if parsed > 0:
                return parsed
        except ValueError:
            pass

    # Fallback to RPC timeout with a small buffer so sync wrappers do not hang
    # indefinitely when remote runtime routes become unhealthy.
    rpc_timeout = (os.environ.get("FLOWNET_RPC_TIMEOUT_SECONDS") or "").strip()
    if rpc_timeout:
        try:
            parsed = float(rpc_timeout)
            if parsed > 0:
                return parsed + 1.0
        except ValueError:
            pass
    return None


def _await_blocking_future_result(fut, *, op_name: str):
    timeout_seconds = _resolve_sync_wait_timeout_seconds()
    if timeout_seconds is None:
        return fut.result()
    try:
        return fut.result(timeout=timeout_seconds)
    except FutureTimeoutError as exc:
        fut.cancel()
        raise TimeoutError(
            f"{op_name} timed out after {timeout_seconds:.1f}s "
            f"(set FLOWNET_SYNC_CALL_TIMEOUT_SECONDS to override)."
        ) from exc


def local_address() -> str:
    return get_runtime_kernel().local_address()


def cluster_view():
    return get_runtime_kernel().require_cluster().get_view()


def resolve_plan(
    *,
    stage_id: str,
    request_id: str,
    num_partitions: int,
    affinity_hint: str | None = None,
    addresses=None,
    merge_addresses=None,
    merge_partitions=None,
    include_merge_address: bool = True,
    merge_sharded: bool = False,
    epoch: int = 1,
    plan_id: str | None = None,
    require_healthy: bool = True,
    locality_order=("local", "zone", "region", "remote"),
    resource_requirements=None,
    allow_resource_fallback: bool = True,
    lease_owner_kind: str | None = None,
    lease_owner_id: str | None = None,
    reservation_timeout_ms: int | None = None,
    autoscale_min_workers: int | None = None,
    autoscale_max_workers: int | None = None,
    autoscale_queue_depth: int | None = None,
    autoscale_target_queue_per_worker: int | None = None,
    autoscale_observed_latency_ms: float | None = None,
    autoscale_latency_budget_ms: float | None = None,
    autoscale_observed_throughput_rps: float | None = None,
    autoscale_target_throughput_rps: float | None = None,
    autoscale_cooldown_ms: int | None = None,
    spillover_stage1_tiers=("local", "zone"),
    spillover_stage2_tiers=("region",),
    spillover_allow_remote: bool = True,
    spillover_max_remote_nodes: int | None = None,
    spillover_ttl_ms: int | None = None,
    spillover_claim_id: str | None = None,
):
    return get_runtime_kernel().require_cluster().resolve_plan(
        stage_id=stage_id,
        request_id=request_id,
        num_partitions=num_partitions,
        affinity_hint=affinity_hint,
        addresses=addresses,
        merge_addresses=merge_addresses,
        merge_partitions=merge_partitions,
        include_merge_address=include_merge_address,
        merge_sharded=merge_sharded,
        epoch=epoch,
        plan_id=plan_id,
        require_healthy=require_healthy,
        locality_order=locality_order,
        resource_requirements=resource_requirements,
        allow_resource_fallback=allow_resource_fallback,
        lease_owner_kind=lease_owner_kind,
        lease_owner_id=lease_owner_id,
        reservation_timeout_ms=reservation_timeout_ms,
        autoscale_min_workers=autoscale_min_workers,
        autoscale_max_workers=autoscale_max_workers,
        autoscale_queue_depth=autoscale_queue_depth,
        autoscale_target_queue_per_worker=autoscale_target_queue_per_worker,
        autoscale_observed_latency_ms=autoscale_observed_latency_ms,
        autoscale_latency_budget_ms=autoscale_latency_budget_ms,
        autoscale_observed_throughput_rps=autoscale_observed_throughput_rps,
        autoscale_target_throughput_rps=autoscale_target_throughput_rps,
        autoscale_cooldown_ms=autoscale_cooldown_ms,
        spillover_stage1_tiers=spillover_stage1_tiers,
        spillover_stage2_tiers=spillover_stage2_tiers,
        spillover_allow_remote=spillover_allow_remote,
        spillover_max_remote_nodes=spillover_max_remote_nodes,
        spillover_ttl_ms=spillover_ttl_ms,
        spillover_claim_id=spillover_claim_id,
    )


def cluster_scheduler_metrics():
    return get_runtime_kernel().require_cluster().metrics_snapshot()


def reset_cluster_scheduler_metrics():
    return get_runtime_kernel().require_cluster().reset_metrics()


def cluster_gossip_stats():
    return get_runtime_kernel().require_cluster().gossip_stats()


def list_runtime_topics():
    return node_control_call("list_runtime_topics")


def list_runtime_topics_on(address: str):
    return node_control_call_on(address, "list_runtime_topics")


def list_runtime_actors():
    return node_control_call("list_runtime_actors")


def list_runtime_actors_on(address: str):
    return node_control_call_on(address, "list_runtime_actors")


def list_runtime_nodes():
    return node_control_call("list_runtime_nodes")


def list_runtime_nodes_on(address: str):
    return node_control_call_on(address, "list_runtime_nodes")


def node_control_call(op: str, *args, **kwargs):
    return _call_node_control_sync(local_address(), op, *args, **kwargs)


def node_control_call_on(address: str, op: str, *args, **kwargs):
    return _call_node_control_sync(address, op, *args, **kwargs)


def lookup_actor(actor_id: str):
    return get_runtime_kernel().require_actors().lookup(actor_id)


def create_actor(actor_class: type, *args, actor_config: Any | None = None, **kwargs):
    if _in_runtime_loop():
        raise RuntimeError(
            "create_actor() cannot block in runtime loop; "
            "avoid synchronous actor creation from flow execution paths."
        )
    fut = asyncio.run_coroutine_threadsafe(
        get_runtime_kernel().require_actors().create_actor(
            actor_class, *args, actor_config=actor_config, **kwargs
        ),
        _runtime_loop(),
    )
    return _await_blocking_future_result(fut, op_name="create_actor")


def create_local_actor(actor_class: type, *args, actor_config: Any | None = None, **kwargs):
    return create_actor(actor_class, *args, actor_config=actor_config, **kwargs)


def create_actor_on(
    address: str,
    actor_class: type,
    *args,
    actor_config: Any | None = None,
    **kwargs,
):
    # Targeted actor creation helper used by API-layer placement policy.
    # It routes actor construction to a specific runtime address without
    # exposing container-management details to the caller.
    return _call_container_method_sync(
        address,
        "create_actor",
        actor_class,
        *args,
        actor_config=actor_config,
        **kwargs,
    )


def _call_container_method_sync(address: str, method: str, *args, **kwargs):
    if _in_runtime_loop():
        raise RuntimeError(
            f"{method}() cannot block in runtime loop; "
            "avoid synchronous container RPC from flow execution paths."
        )
    target = str(address or "").strip()
    if not target:
        raise ValueError(f"{method}() requires a non-empty target address.")
    fut = asyncio.run_coroutine_threadsafe(
        get_runtime_kernel().require_actors().call(
            ActorMethodRef(address=target, actor_id="__container__", method=method),
            *args,
            **kwargs,
        ),
        _runtime_loop(),
    )
    return _await_blocking_future_result(
        fut,
        op_name=f"container_rpc:{method}@{target}",
    )


def _call_node_control_sync(address: str, op: str, *args, **kwargs):
    if _in_runtime_loop():
        raise RuntimeError(
            f"node_control_call({op}) cannot block in runtime loop; "
            "avoid synchronous node-control RPC from flow execution paths."
        )
    target = str(address or "").strip()
    if not target:
        raise ValueError("node_control_call requires a non-empty target address.")
    fut = asyncio.run_coroutine_threadsafe(
        get_runtime_kernel().require_node_control().call_on(
            target,
            op,
            *args,
            **kwargs,
        ),
        _runtime_loop(),
    )
    return _await_blocking_future_result(
        fut,
        op_name=f"node_control:{op}@{target}",
    )


def make_object_actor(actor_object: Any, actor_config: Any | None = None):
    return get_runtime_kernel().require_actors().make_object_actor(
        actor_object,
        actor_config=actor_config,
    )



def handle_actor_call(rmf: ActorMethodRef, *args, **kwargs):
    with _system_calls_scope(True):
        return asyncio.run_coroutine_threadsafe(
            get_runtime_kernel().require_actors().call(rmf, *args, **kwargs),
            _runtime_loop(),
        )


def handle_flow_request(request):
    with _system_calls_scope(True):
        return asyncio.run_coroutine_threadsafe(
            get_runtime_kernel().require_flows().handle(request),
            _runtime_loop(),
        )


def submit_flow(flow_def_or_task, *, ingress=None, egress=None, run_config=None):
    with _system_calls_scope(True):
        return get_runtime_kernel().require_flows().submit_flow(
            flow_def_or_task,
            ingress=ingress,
            egress=egress,
            run_config=run_config,
        )


def lookup_flow_run(run_id: str):
    with _system_calls_scope(True):
        return get_runtime_kernel().require_flows().lookup_run(run_id)


def register_flow_program(
    flow_def_or_task,
    *,
    flow_uri: str,
    version: str,
    metadata: dict[str, Any] | None = None,
    io_contract: dict[str, Any] | None = None,
):
    with _system_calls_scope(True):
        return get_runtime_kernel().require_flows().register_flow_program(
            flow_def_or_task,
            flow_uri=flow_uri,
            version=version,
            metadata=metadata,
            io_contract=io_contract,
        )


def register_flow_program_on(
    address: str,
    flow_def_or_task,
    *,
    flow_uri: str,
    version: str,
    metadata: dict[str, Any] | None = None,
    io_contract: dict[str, Any] | None = None,
):
    return _call_container_method_sync(
        address,
        "register_flow_program",
        flow_def_or_task,
        flow_uri=flow_uri,
        version=version,
        metadata=metadata,
        io_contract=io_contract,
    )


def list_flow_programs(
    *,
    prefix: str | None = None,
    tags: dict[str, Any] | None = None,
    limit: int | None = None,
):
    with _system_calls_scope(True):
        return get_runtime_kernel().require_flows().list_flow_programs(
            prefix=prefix,
            tags=tags,
            limit=limit,
        )


def list_flow_programs_on(
    address: str,
    *,
    prefix: str | None = None,
    tags: dict[str, Any] | None = None,
    limit: int | None = None,
):
    return _call_container_method_sync(
        address,
        "list_flow_programs",
        prefix=prefix,
        tags=tags,
        limit=limit,
    )


def get_flow_program(flow_uri: str, version: str | None = None):
    with _system_calls_scope(True):
        return get_runtime_kernel().require_flows().get_flow_program(
            flow_uri,
            version=version,
        )


def get_flow_program_on(
    address: str,
    flow_uri: str,
    version: str | None = None,
):
    return _call_container_method_sync(
        address,
        "get_flow_program",
        flow_uri,
        version=version,
    )


def delete_flow_program(flow_uri: str, version: str | None = None):
    with _system_calls_scope(True):
        return get_runtime_kernel().require_flows().delete_flow_program(
            flow_uri,
            version=version,
        )


def delete_flow_program_on(address: str, flow_uri: str, version: str | None = None):
    return _call_container_method_sync(
        address,
        "delete_flow_program",
        flow_uri,
        version=version,
    )


def submit_flow_program(
    flow_uri: str,
    version: str | None = None,
    *,
    bindings: dict[str, Any] | None = None,
    ingress=None,
    egress=None,
    run_config=None,
):
    with _system_calls_scope(True):
        return get_runtime_kernel().require_flows().submit_flow_program(
            flow_uri,
            version=version,
            bindings=bindings,
            ingress=ingress,
            egress=egress,
            run_config=run_config,
        )


def submit_flow_program_on(
    address: str,
    flow_uri: str,
    version: str | None = None,
    *,
    bindings: dict[str, Any] | None = None,
    ingress=None,
    egress=None,
    run_config=None,
):
    # Cross-runtime flow-program submission should return a serializable
    # acceptance descriptor rather than a live FlowRunRef object.
    return node_control_call_on(
        address,
        "submit_flow_program_accept",
        flow_uri,
        version=version,
        bindings=bindings,
        ingress=ingress,
        egress=egress,
        run_config=run_config,
    )


def stream_tracker_stats():
    return get_runtime_kernel().require_flows().stream_tracker_stats()


def stream_tracker_request_status(request_id: str):
    return get_runtime_kernel().require_flows().stream_tracker_request_status(request_id)


def get_runtime_loop():
    return loops.get_runtime_loop()


def sync_wait_asyncio_future(fut: asyncio.Future):
    loop = fut.get_loop()

    async def wrap():
        return await fut

    return asyncio.run_coroutine_threadsafe(wrap(), loop).result()


__all__ = [
    "submit_runtime_coro",
    "local_address",
    "cluster_view",
    "resolve_plan",
    "cluster_scheduler_metrics",
    "reset_cluster_scheduler_metrics",
    "cluster_gossip_stats",
    "node_control_call",
    "node_control_call_on",
    "list_runtime_topics",
    "list_runtime_topics_on",
    "list_runtime_actors",
    "list_runtime_actors_on",
    "list_runtime_nodes",
    "list_runtime_nodes_on",
    "lookup_actor",
    "create_actor",
    "create_local_actor",
    "create_actor_on",
    "make_object_actor",
    "handle_actor_call",
    "handle_flow_request",
    "submit_flow",
    "lookup_flow_run",
    "register_flow_program",
    "register_flow_program_on",
    "list_flow_programs",
    "list_flow_programs_on",
    "get_flow_program",
    "get_flow_program_on",
    "delete_flow_program",
    "delete_flow_program_on",
    "submit_flow_program",
    "submit_flow_program_on",
    "stream_tracker_stats",
    "stream_tracker_request_status",
    "get_runtime_loop",
    "sync_wait_asyncio_future",
]
