"""
sage.platform.runtime.adapters.flutty_adapter - Flutty Runtime Adapter

Layer: L2 (sage-platform)

This module provides ``FluttyRuntimeAdapter`` – the concrete implementation of
``RuntimeBackendProtocol`` that delegates to flutty (``flutty.*``),
the successor to sageFlownet.

Architecture
------------
- All imports of ``flutty.*`` are **lazy** (inside methods) so that importing
  this module does not fail when flutty is not installed.
- Actors are created locally via ``LocalActorRegistry.register_local_actor()``.
  In lightweight (single-process) mode there is **no network boot** —
  actor method calls are simple in-process Python function calls.
- For full distributed mode pass ``config={"mode": "cluster", ...}`` to
  ``start()``, which calls ``flutty.init_local()`` and attaches a
  ``V1Session``.

Execution Model (local / lightweight)
--------------------------------------
``create(actor_class, *args, **kwargs)``
    Instantiates *actor_class*, registers the object in a local
    ``LocalActorRegistry``, and returns a ``_FluttyActorHandle``.

``_FluttyActorHandle.get_method(name)``
    Returns a ``_FluttyMethodRef`` that directly resolves the registered
    actor object and calls its Python method — zero network overhead.

Usage
-----
::

    from sage.platform.runtime.adapters.flutty_adapter import (
        FluttyRuntimeAdapter,
        get_flutty_adapter,
    )

    backend = get_flutty_adapter()   # returns/starts the process singleton
    handle  = backend.create(MyActorClass, *init_args)

    # Synchronous call
    result = handle.get_method("process").call(payload)

    # Asynchronous call
    future = handle.get_method("process").async_call(payload)
    result = future.result(timeout=30.0)

Migration from sageFlownet
--------------------------
This adapter replaces ``FlownetRuntimeAdapter``.  Callers that obtained a
backend via ``get_flownet_adapter()`` should switch to ``get_flutty_adapter()``.
``get_flownet_adapter()`` in ``flownet_adapter.py`` is now deprecated and
delegates here.

References
----------
- Protocol definition:  ``sage.platform.runtime.protocol``
- Predecessor adapter:  flownet_adapter (deprecated)
- flutty repo:          https://github.com/intellistream/flutty
"""

from __future__ import annotations

import concurrent.futures
from threading import Lock
from typing import Any

from sage.platform.runtime.protocol import (
    ActorHandleProtocol,
    FlowRunHandleProtocol,
    MethodCallFuture,
    MethodRefProtocol,
    NodeInfoProtocol,
    RuntimeBackendProtocol,
)

__all__ = [
    "FluttyRuntimeAdapter",
    "get_flutty_adapter",
]


# ---------------------------------------------------------------------------
# Guard helper
# ---------------------------------------------------------------------------


def _require_flutty(operation: str) -> None:
    """Raise a clear ImportError when flutty is not installed."""
    try:
        import flutty  # noqa: F401
    except ImportError as exc:
        raise ImportError(
            f"FluttyRuntimeAdapter.{operation}() requires flutty to be installed.\n"
            "Install it with:  pip install flutty\n"
            f"Original error: {exc}"
        ) from exc


# ---------------------------------------------------------------------------
# Shared async executor
# ---------------------------------------------------------------------------

_async_executor: concurrent.futures.ThreadPoolExecutor | None = None
_async_executor_lock: Lock = Lock()


def _get_async_executor() -> concurrent.futures.ThreadPoolExecutor:
    global _async_executor
    with _async_executor_lock:
        if _async_executor is None:
            _async_executor = concurrent.futures.ThreadPoolExecutor(
                max_workers=32,
                thread_name_prefix="sage_flutty_async",
            )
    return _async_executor


# ---------------------------------------------------------------------------
# MethodCallFuture
# ---------------------------------------------------------------------------


class _FluttyMethodCallFuture(MethodCallFuture):
    """Wraps a ``concurrent.futures.Future`` as a ``MethodCallFuture``."""

    __slots__ = ("_fut",)

    def __init__(self, fut: concurrent.futures.Future) -> None:  # type: ignore[type-arg]
        self._fut = fut

    def result(self, timeout: float | None = None) -> Any:
        try:
            return self._fut.result(timeout=timeout)
        except concurrent.futures.TimeoutError as exc:
            raise TimeoutError(
                f"Actor method call did not complete within {timeout}s."
            ) from exc
        except concurrent.futures.CancelledError as exc:
            raise RuntimeError("Actor method call was cancelled.") from exc

    def cancel(self) -> bool:
        return self._fut.cancel()

    @property
    def done(self) -> bool:
        return self._fut.done()


# ---------------------------------------------------------------------------
# Protocol wrappers
# ---------------------------------------------------------------------------


class _FluttyMethodRef(MethodRefProtocol):
    """Calls a named method on a locally registered flutty actor object.

    This is a **direct in-process call** — no serialisation, no network.
    For remote dispatch, extend this to use ``ActorAPI.call_remote()``.
    """

    __slots__ = ("_actor_id", "_method_name", "_registry")

    def __init__(self, actor_id: str, method_name: str, registry: Any) -> None:
        self._actor_id = actor_id
        self._method_name = method_name
        self._registry = registry

    def call(self, *args: Any, **kwargs: Any) -> Any:
        """Resolve the actor object and invoke the method directly."""
        record = self._registry.resolve_local_actor(self._actor_id)
        method = getattr(record.object, self._method_name)
        return method(*args, **kwargs)

    def async_call(self, *args: Any, **kwargs: Any) -> MethodCallFuture:
        """Submit the call to the shared thread-pool and return a future."""
        executor = _get_async_executor()
        fut = executor.submit(self.call, *args, **kwargs)
        return _FluttyMethodCallFuture(fut)

    def cancel(self) -> bool:
        """Cancellation not supported for in-process calls."""
        return False


class _FluttyActorHandle(ActorHandleProtocol):
    """Wraps a locally registered actor as an ``ActorHandleProtocol``."""

    def __init__(self, actor_id: str, registry: Any) -> None:
        object.__setattr__(self, "_actor_id", actor_id)
        object.__setattr__(self, "_registry", registry)

    def get_method(self, name: str) -> MethodRefProtocol:
        return _FluttyMethodRef(self._actor_id, name, self._registry)

    def cancel(self) -> bool:
        """Remove the actor from the local registry (best-effort stop)."""
        try:
            deleted = self._registry.delete_local_actor(self._actor_id)
            return bool(deleted)
        except Exception:  # noqa: BLE001
            return False


class _FluttyFlowRunHandle(FlowRunHandleProtocol):
    """Wraps a flutty flow-run result dict or endpoint request."""

    __slots__ = ("_handle",)

    def __init__(self, handle: Any) -> None:
        self._handle = handle

    def call(self, *args: Any, **kwargs: Any) -> Any:
        call_fn = getattr(self._handle, "call", None)
        if not callable(call_fn):
            raise TypeError(
                f"Flutty flow handle {type(self._handle)!r} does not expose .call()."
            )
        return call_fn(*args, **kwargs)

    def cancel(self) -> None:
        cancel_fn = getattr(self._handle, "cancel", None)
        if cancel_fn is None:
            raise RuntimeError(
                "Flutty flow run handle does not support cancel()."
            )
        cancel_fn()


class _FluttyNodeInfo(NodeInfoProtocol):
    """A static local-node snapshot satisfying ``NodeInfoProtocol``."""

    __slots__ = ("_node_id", "_address", "_resources")

    def __init__(
        self,
        node_id: str = "local",
        address: str = "127.0.0.1",
        resources: dict[str, Any] | None = None,
    ) -> None:
        self._node_id = node_id
        self._address = address
        self._resources: dict[str, Any] = resources or {}

    @property
    def node_id(self) -> str:
        return self._node_id

    @property
    def address(self) -> str:
        return self._address

    @property
    def is_schedulable(self) -> bool:
        return bool(self._resources.get("schedulable", True))

    @property
    def resource_summary(self) -> dict[str, Any]:
        return dict(self._resources)


# ---------------------------------------------------------------------------
# FluttyRuntimeAdapter
# ---------------------------------------------------------------------------

_DEFAULT_LOCAL_ADDRESS = "127.0.0.1:19931"


class FluttyRuntimeAdapter(RuntimeBackendProtocol):
    """Adapter that satisfies ``RuntimeBackendProtocol`` using flutty.

    Lightweight mode (default)
    --------------------------
    Only a ``LocalActorRegistry`` and the flutty ``ActorAPI`` are started.
    No network ports are opened.  All actor method calls are direct Python
    function invocations in the same process.

    Cluster mode
    ------------
    Pass ``config={"mode": "cluster", "local_address": "host:port"}`` to
    ``start()``.  This calls ``flutty.init_local()`` for a full in-process
    cluster node with RPC and topic routing.

    Thread Safety
    -------------
    ``start()`` is idempotent and lock-protected.
    ``stop()`` tears down the registry and the optional session.
    """

    def __init__(self) -> None:
        self._started: bool = False
        self._session: Any | None = None         # V1Session (cluster mode only)
        self._registry: Any | None = None        # LocalActorRegistry
        self._actor_api: Any | None = None       # ActorAPI
        self._lock: Lock = Lock()

    # ------------------------------------------------------------------
    # Lifecycle
    # ------------------------------------------------------------------

    def start(self, config: Any | None = None) -> None:
        """Boot the flutty adapter (idempotent).

        Args:
            config: Optional dict.  Recognised keys:
                - ``mode``           : ``"lightweight"`` (default) or ``"cluster"``
                - ``local_address``  : bind address for cluster mode
                                       (default ``"127.0.0.1:19931"``)
        """
        with self._lock:
            if self._started:
                return
            _require_flutty("start")

            cfg: dict[str, Any] = dict(config) if isinstance(config, dict) else {}
            mode = str(cfg.get("mode", "lightweight")).strip().lower()
            local_address = str(cfg.get("local_address", _DEFAULT_LOCAL_ADDRESS)).strip()

            from flutty.runtime.actors import ActorAPI
            from flutty.runtime.actors.registry import LocalActorRegistry

            self._registry = LocalActorRegistry(local_address=local_address)
            self._actor_api = ActorAPI(
                local_address=local_address,
                registry=self._registry,
            )

            if mode == "cluster":
                import flutty
                self._session = flutty.init_local(
                    owner="sage-kernel",
                    local_address=local_address,
                )

            # Wire the flutty exception-handler scope into SAGE's
            # exception_handler hook slot (sage.kernel.flow.exception_handler).
            self._register_exception_hooks()

            self._started = True

    def stop(self) -> None:
        """Tear down the adapter and release all actors."""
        with self._lock:
            if not self._started:
                return
            if self._session is not None:
                try:
                    self._session.shutdown(wait=True)
                except Exception:  # noqa: BLE001
                    pass
                self._session = None
            self._registry = None
            self._actor_api = None
            self._started = False

    def _assert_started(self, op: str) -> None:
        if not self._started:
            raise RuntimeError(
                f"FluttyRuntimeAdapter.{op}() called before start(). "
                "Call start() first or use get_flutty_adapter() which auto-starts."
            )

    @staticmethod
    def _register_exception_hooks() -> None:
        """Wire flutty's flow compilation scope into SAGE's exception handler."""
        try:
            from flutty.compiler.exception_scope import (
                flow_exception_handler as _feh,
                flow_program_scope as _fps,
            )
            from sage.kernel.flow.exception_handler import (  # lazy L3 import
                register_exception_handler_hook,
            )

            # push = enter flutty's flow_exception_handler scope with a handler
            # pop  = exit that scope (no-op stub; managed as context in flutty)
            def _push(handler: Any) -> None:
                _feh(handler).__enter__()

            def _pop() -> None:
                pass  # flutty manages scope via context managers; see _feh.__exit__

            register_exception_handler_hook(_push, _pop)
        except Exception:  # noqa: BLE001
            # Best-effort: if L3 is not available (standalone platform tests) skip
            pass

    # ------------------------------------------------------------------
    # Actor creation
    # ------------------------------------------------------------------

    def create(
        self,
        actor_class: type,
        /,
        *args: Any,
        actor_config: Any | None = None,
        **kwargs: Any,
    ) -> ActorHandleProtocol:
        """Instantiate *actor_class* locally and register it.

        Args:
            actor_class: The actor class to instantiate.
            *args, **kwargs: Forwarded to ``actor_class.__init__``.
            actor_config: Optional flutty actor config dict.

        Returns:
            A ``_FluttyActorHandle`` wrapping the registered actor.
        """
        self._assert_started("create")
        instance = actor_class(*args, **kwargs)
        actor_id = self._registry.register_local_actor(
            instance, config=actor_config
        )
        return _FluttyActorHandle(actor_id, self._registry)

    # ------------------------------------------------------------------
    # Flow submission (cluster / session mode)
    # ------------------------------------------------------------------

    def submit(
        self,
        flow_obj: Any,
        *,
        ingress: Any | None = None,
        egress: Any | None = None,
        run_config: Any | None = None,
    ) -> FlowRunHandleProtocol:
        """Submit a flutty ``FlowDeclaration`` via the active session.

        Requires cluster mode (``config={"mode": "cluster"}`` at start-time).
        For purely local pipeline execution, prefer ``pipeline_compiler``
        which dispatches actor method calls directly.

        Args:
            flow_obj: A ``flutty.api.declarations.FlowDeclaration`` or any
                      object accepted by ``flutty.compiler.compile_flow_program``.
            ingress: In-topic URI string for the flow's input binding.
            egress:  Out-topic URI string for the flow's output binding.
            run_config: Optional run-config dict forwarded to flutty.
        """
        self._assert_started("submit")
        if self._session is None:
            raise RuntimeError(
                "FluttyRuntimeAdapter.submit() requires cluster mode.\n"
                "Start with config={'mode': 'cluster'} or use pipeline_compiler "
                "for local in-process pipelines."
            )
        # For FlowDeclaration objects use the endpoint model.
        endpoint_fn = getattr(flow_obj, "endpoint", None)
        if callable(endpoint_fn):
            kwargs: dict[str, Any] = {}
            if ingress is not None:
                kwargs["in_topic"] = ingress
            if egress is not None:
                kwargs["out_topic"] = egress
            endpoint = endpoint_fn(**kwargs)
            return _FluttyFlowRunHandle(endpoint)

        # Generic: try session.submit_flow_program if a URI is known.
        flow_uri = getattr(flow_obj, "flow_uri", None) or getattr(flow_obj, "uri", None)
        if flow_uri and self._session is not None:
            result = self._session.submit_flow_program(
                str(flow_uri),
                ingress=ingress,
                egress=egress,
                run_config=dict(run_config) if run_config is not None else None,
            )
            return _FluttyFlowRunHandle(result)

        raise TypeError(
            f"FluttyRuntimeAdapter.submit() does not know how to submit "
            f"{type(flow_obj)!r}.  Pass a FlowDeclaration."
        )

    # ------------------------------------------------------------------
    # Node information
    # ------------------------------------------------------------------

    def list_nodes(self) -> list[NodeInfoProtocol]:
        """Return node info from the active session or a local stub."""
        self._assert_started("list_nodes")

        if self._session is not None:
            # Cluster mode: query the runtime inspector
            try:
                inspector_fn = getattr(self._session, "_runtime_inspector", None)
                if callable(inspector_fn):
                    inspector = inspector_fn()
                    snapshot = inspector.cluster_view_snapshot(schema="v1")
                    nodes_raw = snapshot.get("nodes", []) if isinstance(snapshot, dict) else []
                    return [
                        _FluttyNodeInfo(
                            node_id=str(n.get("node_id", "unknown")),
                            address=str(n.get("address", "unknown")),
                            resources=n.get("resources", {}),
                        )
                        for n in nodes_raw
                    ]
            except Exception:  # noqa: BLE001
                pass

        # Lightweight mode: return a single "local" node stub
        return [_FluttyNodeInfo()]


# ---------------------------------------------------------------------------
# Singleton accessor
# ---------------------------------------------------------------------------

_adapter_singleton: FluttyRuntimeAdapter | None = None
_adapter_lock: Lock = Lock()


def get_flutty_adapter(*, auto_start: bool = True) -> FluttyRuntimeAdapter:
    """Return (and optionally start) the process-global ``FluttyRuntimeAdapter``.

    Args:
        auto_start: If ``True`` (default), calls :meth:`~FluttyRuntimeAdapter.start`
                    on first access using lightweight mode defaults.

    Returns:
        The singleton ``FluttyRuntimeAdapter`` instance.
    """
    global _adapter_singleton
    with _adapter_lock:
        if _adapter_singleton is None:
            _adapter_singleton = FluttyRuntimeAdapter()
        if auto_start and not _adapter_singleton._started:
            _adapter_singleton.start()
        return _adapter_singleton
