"""The ``Transport`` seam — the boundary between the SDK clients and any backend.

This is the single abstraction that decouples :class:`~mgf.vm.sdk.client.VmManagerClient`
(and its async twin) from *how* requests reach the control plane. Three concrete
transports satisfy it across phases:

* ``InProcessTransport`` — VManager Local (next slice); wires the client straight
  to in-process ``mgf.vm.core`` adapters. No daemons, no network.
* ``GrpcTransport`` — VManager Fleet (Phase 1); speaks ``vmanager.api.v1`` over
  gRPC + TLS (``api.md`` §4).
* test fakes — e.g. ``FakeGrpcTransport`` in the test suite, which records
  submitted ops and returns canned handles.

The seam is expressed in **engine- and wire-neutral core types only**
(:mod:`mgf.vm.core.contracts`, :mod:`mgf.vm.core.ids`): an
:class:`~mgf.vm.core.contracts.OperationHandle` is the unit of async work, and
reads return plain data. No libvirt, no gRPC, no protobuf leaks across this line
(enforced by the import-linter contract in ``pyproject.toml``).

Two mirror Protocols:

* :class:`Transport` — the sync surface (blocking calls, ``Iterator`` streams).
* :class:`AsyncTransport` — the async surface (``async def`` /
  ``AsyncIterator``).

Both map 1:1 to ``api.md`` §4.2: every mutating RPC is two-step (``submit_op`` →
``watch_op`` / ``get_op``), reads (``Get*`` / ``List*``) are unary via
:meth:`Transport.read`, and resource watches stream via
:meth:`Transport.watch_stream`.

Errors raised across the seam are the core ``TransportError`` tree
(:class:`~mgf.vm.core.exceptions.TransportError` and its subclasses); transports
MUST NOT define their own. They are re-exported here purely for the convenience
of transport authors.
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Protocol, runtime_checkable

# Re-exported for transport-author convenience; NOT redefined here (these are
# the canonical core types — see api.md §7 error carriage).
from mgf.vm.core.exceptions import (
    TransportCancelled,
    TransportConnectionError,
    TransportError,
    TransportTimeout,
)

if TYPE_CHECKING:
    from collections.abc import AsyncIterator, Iterator

    from mgf.vm.core.contracts import (
        EventGap,
        OperationHandle,
        OperationKind,
        VmEvent,
    )
    from mgf.vm.core.ids import OperationId

__all__ = [
    "AsyncTransport",
    "Transport",
    "TransportCancelled",
    "TransportConnectionError",
    "TransportError",
    "TransportTimeout",
]


@runtime_checkable
class Transport(Protocol):
    """The synchronous client↔backend seam (``api.md`` §4.2).

    A blocking surface: mutating RPCs are submitted via :meth:`submit_op`
    (returns immediately with a ``PENDING``/in-flight handle) and resolved via
    :meth:`watch_op` (streams transitions to a terminal state) or polled via
    :meth:`get_op`. Reads are unary (:meth:`read`); resource subscriptions
    stream via :meth:`watch_stream`.

    Implementations MUST be safe to share across threads for reads; per-op
    ordering guarantees follow ``api.md`` §8.1 (causal order per stream).
    """

    def submit_op(
        self,
        kind: OperationKind,
        payload: object,
        *,
        client_op_id: str,
        token: object | None = None,
    ) -> OperationHandle:
        """Submit a mutating operation; return its handle immediately.

        Maps to one mutating ``ControlPlane`` RPC (``CreateVm``, ``StartVm``, …).
        ``client_op_id`` is the caller-generated UUID the backend dedupes on
        (``api.md`` §4.2; D-G5): a retried submit with the same id returns the
        same operation. ``payload`` is the op-specific request (a ``VmSpec``,
        a ``(DomainId, force)`` tuple, …) — opaque to the seam. ``token`` is an
        optional cancellation token threaded to the backend.

        Returns a ``PENDING`` or ``RUNNING`` :class:`OperationHandle`; the caller
        learns the outcome via :meth:`watch_op` / :meth:`get_op`.
        """
        ...

    def get_op(self, op_id: OperationId) -> OperationHandle:
        """Fetch the current handle for ``op_id`` (``GetOperation``; unary).

        Raises a ``TransportError`` subclass on transport failure. Returns the
        handle in whatever status it currently holds (possibly terminal).
        """
        ...

    def watch_op(self, op_id: OperationId) -> Iterator[OperationHandle]:
        """Stream every status transition for ``op_id`` until terminal (``WatchOp``).

        Yields handles in causal order (``api.md`` §8.1); the iterator is
        exhausted once a terminal status (``SUCCEEDED`` / ``FAILED`` /
        ``CANCELLED``) is emitted. Closing the iterator releases the
        subscription deterministically.
        """
        ...

    def read(self, query: object) -> object:
        """Execute a unary read (``Get*`` / ``List*``); return data directly.

        Reads carry no operation handle (``api.md`` §4.2): they are synchronous
        as far as the protocol is concerned. ``query`` is the op-specific request
        (a ``VmId``, a pool name, …); the return is the op-specific data (a
        ``Vm``, a ``list[PoolInfo]``, …). The client narrows the return type at
        the call site.
        """
        ...

    def watch_stream(self, subject: object) -> Iterator[VmEvent | EventGap]:
        """Stream resource events for ``subject`` (``WatchVm``-class; server stream).

        Yields :class:`~mgf.vm.core.contracts.VmEvent` in causal order, with a
        synthetic :class:`~mgf.vm.core.contracts.EventGap` when the server's
        retention window elapsed and the client must reconcile (``api.md`` §8.1).
        Closing the iterator releases the subscription.
        """
        ...

    def close(self) -> None:
        """Release transport resources (connections, threads). Idempotent."""
        ...


@runtime_checkable
class AsyncTransport(Protocol):
    """The asynchronous mirror of :class:`Transport` (``api.md`` §4.2).

    Identical semantics; ``async def`` calls and ``AsyncIterator`` streams.
    The B7 async surface needs a one-shot poll (``get_operation``), so this
    mirror carries an async :meth:`get_op` alongside :meth:`watch_op` — the
    follow-up the foundation report flagged.
    """

    async def submit_op(
        self,
        kind: OperationKind,
        payload: object,
        *,
        client_op_id: str,
        token: object | None = None,
    ) -> OperationHandle:
        """Async mirror of :meth:`Transport.submit_op`."""
        ...

    async def get_op(self, op_id: OperationId) -> OperationHandle:
        """Async mirror of :meth:`Transport.get_op` (one-shot poll)."""
        ...

    def watch_op(self, op_id: OperationId) -> AsyncIterator[OperationHandle]:
        """Async mirror of :meth:`Transport.watch_op`.

        Returns an ``AsyncIterator`` (not a coroutine): ``async for h in
        transport.watch_op(op_id): ...``.
        """
        ...

    async def read(self, query: object) -> object:
        """Async mirror of :meth:`Transport.read`."""
        ...

    def watch_stream(self, subject: object) -> AsyncIterator[VmEvent | EventGap]:
        """Async mirror of :meth:`Transport.watch_stream`."""
        ...

    async def aclose(self) -> None:
        """Async mirror of :meth:`Transport.close`. Idempotent."""
        ...
