"""MCP transport over MLS-encrypted Skytale channels.

Provides two transports:

- :class:`SkytaleTransport` — low-level encrypted JSON-RPC transport (unchanged
  public API from v1).  Each JSON-RPC message is serialized, wrapped in an
  :class:`Envelope` tagged with :attr:`Protocol.MCP`, and sent through an
  MLS-encrypted channel.

- :class:`EncryptedStreamableHTTPTransport` — wraps MCP's Streamable HTTP
  transport (the replacement for the deprecated SSE transport) with MLS
  encryption.  A single HTTP endpoint handles both request/response and
  server-initiated messages.  Payloads are encrypted before leaving the
  SDK and decrypted on arrival.

This module is *not* the same as ``_mcp.py`` (which exposes Skytale operations
as MCP tools).  This module provides Skytale **as** the transport layer for
arbitrary MCP protocol messages.

Usage (Streamable HTTP with encryption)::

    import asyncio
    from skytale_sdk.channels import SkytaleChannelManager
    from skytale_sdk.integrations._mcp_transport import (
        EncryptedStreamableHTTPTransport,
    )

    mgr = SkytaleChannelManager(identity=b"agent-1")
    mgr.create("org/ns/mcp-rpc")

    transport = EncryptedStreamableHTTPTransport(
        url="https://mcp-server.example.com/mcp",
        channel_manager=mgr,
        channel="org/ns/mcp-rpc",
    )

    async def main():
        async with transport.connect() as (read_stream, write_stream):
            # read_stream / write_stream follow MCP transport protocol;
            # all payloads are MLS-encrypted transparently.
            pass

    asyncio.run(main())

Usage (raw encrypted channel transport — unchanged)::

    import asyncio
    from skytale_sdk.channels import SkytaleChannelManager
    from skytale_sdk.integrations._mcp_transport import SkytaleTransport

    mgr = SkytaleChannelManager(identity=b"agent-1")
    mgr.create("org/ns/mcp-rpc")
    transport = SkytaleTransport(mgr, "org/ns/mcp-rpc")

    async def main():
        await transport.write({"jsonrpc": "2.0", "method": "ping", "id": 1})
        response = await transport.read()
        print(response)
        await transport.close()

    asyncio.run(main())
"""

from __future__ import annotations

import asyncio
import base64
import hashlib
import json
import logging
import os
import secrets
import time
import urllib.parse
from contextlib import asynccontextmanager
from dataclasses import dataclass, field
from typing import (
    Any,
    AsyncIterator,
    Callable,
    Dict,
    List,
    Optional,
    Tuple,
)

from skytale_sdk.channels import SkytaleChannelManager
from skytale_sdk.envelope import Envelope, Protocol
from skytale_sdk.errors import AuthError, TransportError

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# OAuth 2.1 with PKCE
# ---------------------------------------------------------------------------

@dataclass
class OAuthConfig:
    """OAuth 2.1 configuration for MCP server authentication.

    Implements the Authorization Code flow with PKCE (RFC 7636) as required
    by the MCP Streamable HTTP specification.  No client secret is needed
    because PKCE replaces the static secret with a per-request code verifier.

    Args:
        client_id: OAuth client ID registered with the MCP server.
        authorization_url: Authorization endpoint URL.
        token_url: Token endpoint URL.
        scopes: Requested OAuth scopes (space-separated or list).
        redirect_uri: Local redirect URI for the authorization callback.
            Defaults to ``http://localhost:19836/callback``.

    Example::

        cfg = OAuthConfig(
            client_id="skytale-agent-1",
            authorization_url="https://auth.example.com/authorize",
            token_url="https://auth.example.com/token",
            scopes=["mcp:read", "mcp:write"],
        )
    """

    client_id: str
    authorization_url: str
    token_url: str
    scopes: List[str] = field(default_factory=lambda: ["mcp"])
    redirect_uri: str = "http://localhost:19836/callback"


@dataclass
class _OAuthTokens:
    """In-memory token store (never logged or serialized to disk)."""

    access_token: str
    refresh_token: Optional[str] = None
    expires_at: float = 0.0

    @property
    def expired(self) -> bool:
        """Return True if the access token has expired or will within 30s.

        Example::

            >>> t = _OAuthTokens("tok", expires_at=0.0)
            >>> t.expired
            True
        """
        return time.monotonic() >= (self.expires_at - 30)


def _generate_pkce_pair() -> Tuple[str, str]:
    """Generate a PKCE code verifier and its S256 challenge.

    Returns a ``(verifier, challenge)`` tuple.  The verifier is a
    cryptographically random 43-character URL-safe string; the challenge
    is its SHA-256 hash, base64url-encoded without padding.

    Returns:
        Tuple of ``(code_verifier, code_challenge)``.

    Example::

        >>> v, c = _generate_pkce_pair()
        >>> len(v) == 43 and len(c) > 0
        True
    """
    verifier = secrets.token_urlsafe(32)
    digest = hashlib.sha256(verifier.encode("ascii")).digest()
    challenge = base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii")
    return verifier, challenge


async def _exchange_authorization_code(
    oauth: OAuthConfig,
    code: str,
    code_verifier: str,
) -> _OAuthTokens:
    """Exchange an authorization code for tokens via the token endpoint.

    Performs the OAuth 2.1 token request with PKCE verification.  The
    request is sent as ``application/x-www-form-urlencoded`` per RFC 6749.

    Args:
        oauth: OAuth configuration.
        code: Authorization code received from the authorization server.
        code_verifier: PKCE code verifier that was used to generate the
            challenge sent in the authorization request.

    Returns:
        An :class:`_OAuthTokens` instance with access and optional refresh
        tokens.

    Raises:
        AuthError: If the token exchange fails.

    Example::

        tokens = await _exchange_authorization_code(cfg, "authcode", "verifier")
    """
    try:
        import httpx
    except ImportError as exc:
        raise ImportError(
            "httpx is required for OAuth 2.1 support in MCP Streamable HTTP "
            "transport. Install it with: pip install httpx"
        ) from exc

    body = {
        "grant_type": "authorization_code",
        "client_id": oauth.client_id,
        "code": code,
        "redirect_uri": oauth.redirect_uri,
        "code_verifier": code_verifier,
    }

    async with httpx.AsyncClient() as client:
        resp = await client.post(
            oauth.token_url,
            data=body,
            headers={"Accept": "application/json"},
        )
        if resp.status_code != 200:
            raise AuthError(
                f"OAuth token exchange failed (HTTP {resp.status_code}): "
                f"{resp.text[:200]}",
                code="oauth_token_exchange_failed",
                http_status=resp.status_code,
            )

        data = resp.json()

    expires_in = data.get("expires_in", 3600)
    return _OAuthTokens(
        access_token=data["access_token"],
        refresh_token=data.get("refresh_token"),
        expires_at=time.monotonic() + expires_in,
    )


async def _refresh_access_token(
    oauth: OAuthConfig,
    refresh_token: str,
) -> _OAuthTokens:
    """Refresh an expired access token using a refresh token.

    Args:
        oauth: OAuth configuration.
        refresh_token: The refresh token from a previous token exchange.

    Returns:
        New :class:`_OAuthTokens` with a fresh access token.

    Raises:
        AuthError: If the refresh request fails.

    Example::

        tokens = await _refresh_access_token(cfg, "old_refresh_token")
    """
    try:
        import httpx
    except ImportError as exc:
        raise ImportError(
            "httpx is required for OAuth 2.1 support. "
            "Install it with: pip install httpx"
        ) from exc

    body = {
        "grant_type": "refresh_token",
        "client_id": oauth.client_id,
        "refresh_token": refresh_token,
    }

    async with httpx.AsyncClient() as client:
        resp = await client.post(
            oauth.token_url,
            data=body,
            headers={"Accept": "application/json"},
        )
        if resp.status_code != 200:
            raise AuthError(
                f"OAuth token refresh failed (HTTP {resp.status_code}): "
                f"{resp.text[:200]}",
                code="oauth_token_refresh_failed",
                http_status=resp.status_code,
            )

        data = resp.json()

    expires_in = data.get("expires_in", 3600)
    return _OAuthTokens(
        access_token=data["access_token"],
        refresh_token=data.get("refresh_token", refresh_token),
        expires_at=time.monotonic() + expires_in,
    )


def build_authorization_url(oauth: OAuthConfig) -> Tuple[str, str]:
    """Build the OAuth 2.1 authorization URL with PKCE challenge.

    Constructs the full authorization URL that the user or automated flow
    should navigate to.  Returns the URL and the code verifier (which must
    be stored until the callback to complete the token exchange).

    Args:
        oauth: OAuth configuration.

    Returns:
        A ``(authorization_url, code_verifier)`` tuple.

    Example::

        url, verifier = build_authorization_url(cfg)
        # Open url in browser, wait for redirect with ?code=...
    """
    verifier, challenge = _generate_pkce_pair()
    params = {
        "response_type": "code",
        "client_id": oauth.client_id,
        "redirect_uri": oauth.redirect_uri,
        "scope": " ".join(oauth.scopes),
        "code_challenge": challenge,
        "code_challenge_method": "S256",
        "state": secrets.token_urlsafe(16),
    }
    url = oauth.authorization_url + "?" + urllib.parse.urlencode(params)
    return url, verifier


# ---------------------------------------------------------------------------
# Encrypted Streamable HTTP transport
# ---------------------------------------------------------------------------

class EncryptedStreamableHTTPTransport:
    """MCP Streamable HTTP transport with MLS encryption.

    Replaces the deprecated SSE transport with the modern Streamable HTTP
    transport (single endpoint, bidirectional).  All JSON-RPC payloads are
    encrypted via MLS before transmission and decrypted on receipt.

    The transport communicates with the MCP server over a single HTTP
    endpoint that supports:
    - POST for client-to-server JSON-RPC requests
    - GET for server-initiated messages (streaming via chunked transfer)

    MLS encryption is applied transparently: outgoing payloads are wrapped
    in an :class:`Envelope` tagged with :attr:`Protocol.MCP`, encrypted
    through the Skytale channel, and base64-encoded into the HTTP body.
    Incoming payloads are base64-decoded, decrypted, and unwrapped.

    Args:
        url: MCP server Streamable HTTP endpoint URL.
        channel_manager: A :class:`SkytaleChannelManager` with the channel
            already created or joined.
        channel: Channel name in ``org/namespace/service`` format.
        oauth: Optional :class:`OAuthConfig` for OAuth 2.1 authentication.
            When provided, the transport handles token acquisition and
            refresh automatically.
        headers: Additional HTTP headers to include in requests.
        timeout: HTTP request timeout in seconds (default 30).

    Example::

        transport = EncryptedStreamableHTTPTransport(
            url="https://mcp.example.com/mcp",
            channel_manager=mgr,
            channel="org/ns/mcp-rpc",
            oauth=OAuthConfig(
                client_id="my-agent",
                authorization_url="https://auth.example.com/authorize",
                token_url="https://auth.example.com/token",
            ),
        )
    """

    def __init__(
        self,
        url: str,
        channel_manager: SkytaleChannelManager,
        channel: str,
        oauth: Optional[OAuthConfig] = None,
        headers: Optional[Dict[str, str]] = None,
        timeout: float = 30.0,
    ) -> None:
        self._url = url
        self._manager = channel_manager
        self._channel = channel
        self._oauth = oauth
        self._extra_headers = headers or {}
        self._timeout = timeout
        self._tokens: Optional[_OAuthTokens] = None
        self._session_id: Optional[str] = None
        self._closed = False

    async def _ensure_auth(self) -> Dict[str, str]:
        """Build HTTP headers including a valid OAuth bearer token.

        If OAuth is configured and the current token is missing or expired,
        attempts to refresh it.  If no refresh token is available, raises
        :class:`AuthError` indicating re-authorization is needed.

        Returns:
            Dict of HTTP headers to include in the request.

        Raises:
            AuthError: If OAuth is configured but no valid token is available
                and automatic refresh is not possible.

        Example::

            headers = await transport._ensure_auth()
        """
        headers = dict(self._extra_headers)

        if self._oauth is None:
            return headers

        if self._tokens is not None and not self._tokens.expired:
            headers["Authorization"] = f"Bearer {self._tokens.access_token}"
            return headers

        # Attempt refresh if we have a refresh token.
        if self._tokens is not None and self._tokens.refresh_token:
            logger.debug("refreshing expired OAuth token for %s", self._url)
            self._tokens = await _refresh_access_token(
                self._oauth, self._tokens.refresh_token
            )
            headers["Authorization"] = f"Bearer {self._tokens.access_token}"
            return headers

        raise AuthError(
            "OAuth authorization required. Call authorize() first or provide "
            "a token via set_tokens().",
            code="oauth_authorization_required",
        )

    def set_tokens(self, access_token: str, refresh_token: Optional[str] = None,
                   expires_in: int = 3600) -> None:
        """Manually set OAuth tokens (e.g. after completing an external auth flow).

        Use this when the authorization code exchange is handled outside the
        transport (for example, by a web application callback handler).

        Args:
            access_token: The OAuth access token.
            refresh_token: Optional refresh token for automatic renewal.
            expires_in: Token lifetime in seconds (default 3600).

        Example::

            transport.set_tokens("eyJ...", refresh_token="dGhp...", expires_in=3600)
        """
        self._tokens = _OAuthTokens(
            access_token=access_token,
            refresh_token=refresh_token,
            expires_at=time.monotonic() + expires_in,
        )

    async def authorize(self, code: str, code_verifier: str) -> None:
        """Complete OAuth 2.1 authorization code exchange with PKCE.

        Call this after the user has completed the authorization flow and
        you have received the authorization code from the callback.

        Args:
            code: Authorization code from the OAuth callback.
            code_verifier: PKCE code verifier from :func:`build_authorization_url`.

        Raises:
            AuthError: If the token exchange fails.

        Example::

            url, verifier = build_authorization_url(oauth_config)
            # ... user completes auth, you receive 'code' ...
            await transport.authorize(code, verifier)
        """
        if self._oauth is None:
            raise AuthError(
                "cannot authorize: no OAuthConfig provided to transport",
                code="oauth_not_configured",
            )
        self._tokens = await _exchange_authorization_code(
            self._oauth, code, code_verifier
        )
        logger.info("OAuth authorization completed for %s", self._url)

    def _encrypt_payload(self, message: Dict[str, Any]) -> bytes:
        """Encrypt a JSON-RPC message through the MLS channel.

        Serializes the message to compact JSON, wraps it in an
        :class:`Envelope` tagged with :attr:`Protocol.MCP`, and routes it
        through the Skytale channel manager's MLS encryption layer via
        :meth:`~SkytaleChannelManager.send_envelope`.  The envelope bytes
        are then base64-encoded for safe HTTP transport.

        This mirrors :meth:`SkytaleTransport.write` — the envelope is sent
        through ``self._manager.send_envelope()`` which performs MLS
        encryption via the underlying Rust MLS engine.

        Args:
            message: JSON-serializable dict (typically a JSON-RPC message).

        Returns:
            Base64-encoded encrypted payload bytes.

        Example::

            encrypted = transport._encrypt_payload({"jsonrpc": "2.0", "method": "ping", "id": 1})
        """
        payload = json.dumps(message, separators=(",", ":")).encode("utf-8")
        envelope = Envelope(
            protocol=Protocol.MCP,
            content_type="application/json",
            payload=payload,
        )
        # Route through the channel manager's MLS encryption layer,
        # mirroring SkytaleTransport.write(). send_envelope() serializes
        # the envelope and passes the bytes through the Rust MLS engine
        # (encrypt + publish to channel).
        self._manager.send_envelope(self._channel, envelope)
        return base64.b64encode(envelope.serialize())

    def _decrypt_payload(self, data: bytes) -> Dict[str, Any]:
        """Decrypt an MLS-encrypted payload received from the channel.

        Receives envelopes from the Skytale channel via
        :meth:`~SkytaleChannelManager.receive_envelopes`, which performs
        MLS decryption through the underlying Rust MLS engine.  Filters
        for :attr:`Protocol.MCP` envelopes and returns the JSON-RPC
        message payload.

        The *data* parameter provides the base64-encoded ciphertext as
        context (e.g., for logging), but actual decryption is performed by
        the channel manager's MLS layer, mirroring how
        :meth:`SkytaleTransport.read` works.

        Args:
            data: Base64-encoded encrypted payload bytes (used as context;
                decryption is performed by the channel manager).

        Returns:
            The decrypted JSON-RPC message as a dict.

        Raises:
            TransportError: If decryption or deserialization fails, or if
                no MCP envelope is received within the timeout.

        Example::

            msg = transport._decrypt_payload(b"base64data...")
        """
        try:
            # Receive MLS-decrypted envelopes from the channel, mirroring
            # SkytaleTransport.read(). receive_envelopes() drains the
            # channel buffer of already-decrypted messages from the Rust
            # MLS engine.
            envelopes = self._manager.receive_envelopes(
                self._channel, timeout=self._timeout,
            )
            for envelope in envelopes:
                if envelope.protocol is not Protocol.MCP:
                    logger.debug(
                        "ignoring non-MCP envelope (protocol=%s) on "
                        "channel %s",
                        envelope.protocol.value,
                        self._channel,
                    )
                    continue
                return json.loads(envelope.payload)
            raise TransportError(
                "no MCP envelope received from channel within timeout",
                code="decrypt_timeout",
            )
        except (ValueError, json.JSONDecodeError) as exc:
            raise TransportError(
                f"failed to decrypt MCP payload: {exc}",
                code="decrypt_failed",
            ) from exc

    async def send(self, message: Dict[str, Any]) -> Optional[Dict[str, Any]]:
        """Send a JSON-RPC message to the MCP server via Streamable HTTP POST.

        The message is MLS-encrypted before transmission.  If the server
        returns a JSON-RPC response, it is decrypted and returned.

        Args:
            message: A JSON-RPC request or notification dict.

        Returns:
            The server's JSON-RPC response (decrypted), or ``None`` for
            notifications that receive no response body.

        Raises:
            RuntimeError: If the transport is closed.
            TransportError: If the HTTP request fails.
            AuthError: If OAuth tokens are missing or expired.

        Example::

            resp = await transport.send({
                "jsonrpc": "2.0", "method": "tools/list", "id": 1
            })
        """
        if self._closed:
            raise RuntimeError("transport closed")

        try:
            import httpx
        except ImportError as exc:
            raise ImportError(
                "httpx is required for Streamable HTTP transport. "
                "Install it with: pip install httpx"
            ) from exc

        headers = await self._ensure_auth()
        headers["Content-Type"] = "application/json"
        headers["Accept"] = "application/json, text/event-stream"

        if self._session_id:
            headers["Mcp-Session-Id"] = self._session_id

        encrypted_body = self._encrypt_payload(message)

        # Wrap the encrypted body in a JSON envelope so the MCP server
        # receives valid JSON with an "encrypted_payload" field.
        request_body = json.dumps({
            "jsonrpc": "2.0",
            "method": message.get("method", ""),
            "id": message.get("id"),
            "params": {
                "encrypted_payload": encrypted_body.decode("ascii"),
            },
        }).encode("utf-8")

        async with httpx.AsyncClient(timeout=self._timeout) as client:
            resp = await client.post(self._url, content=request_body, headers=headers)

        if resp.status_code == 401 and self._oauth:
            raise AuthError(
                "OAuth token rejected by MCP server (HTTP 401). "
                "Re-authorize with authorize() or set_tokens().",
                code="oauth_token_rejected",
                http_status=401,
            )

        if resp.status_code not in (200, 202, 204):
            raise TransportError(
                f"Streamable HTTP request failed (HTTP {resp.status_code}): "
                f"{resp.text[:200]}",
                code="http_request_failed",
                http_status=resp.status_code,
            )

        # Capture session ID from the server if provided.
        if "mcp-session-id" in resp.headers:
            self._session_id = resp.headers["mcp-session-id"]

        if resp.status_code == 204 or not resp.content:
            return None

        # Check if the response contains encrypted payload.
        try:
            resp_data = resp.json()
            if "encrypted_payload" in resp_data.get("result", {}):
                return self._decrypt_payload(
                    resp_data["result"]["encrypted_payload"].encode("ascii")
                )
            return resp_data
        except (json.JSONDecodeError, KeyError):
            return None

    async def open_stream(self) -> AsyncIterator[Dict[str, Any]]:
        """Open a GET stream for server-initiated messages.

        Connects to the Streamable HTTP endpoint via GET to receive
        server-pushed messages.  Each message is MLS-decrypted before
        being yielded.

        Yields:
            Decrypted JSON-RPC messages from the server.

        Raises:
            RuntimeError: If the transport is closed.
            TransportError: If the streaming connection fails.

        Example::

            async for msg in transport.open_stream():
                print(msg["method"])
        """
        if self._closed:
            raise RuntimeError("transport closed")

        try:
            import httpx
        except ImportError as exc:
            raise ImportError(
                "httpx is required for Streamable HTTP transport. "
                "Install it with: pip install httpx"
            ) from exc

        headers = await self._ensure_auth()
        headers["Accept"] = "text/event-stream"

        if self._session_id:
            headers["Mcp-Session-Id"] = self._session_id

        async with httpx.AsyncClient(timeout=None) as client:
            async with client.stream("GET", self._url, headers=headers) as resp:
                if resp.status_code != 200:
                    raise TransportError(
                        f"Streamable HTTP GET failed (HTTP {resp.status_code})",
                        code="stream_open_failed",
                        http_status=resp.status_code,
                    )

                if "mcp-session-id" in resp.headers:
                    self._session_id = resp.headers["mcp-session-id"]

                event_data = ""
                async for line in resp.aiter_lines():
                    if self._closed:
                        return

                    if line.startswith("data: "):
                        event_data += line[6:]
                    elif line == "" and event_data:
                        # End of SSE event — parse and decrypt.
                        try:
                            msg = json.loads(event_data)
                            if "encrypted_payload" in msg.get("params", {}):
                                yield self._decrypt_payload(
                                    msg["params"]["encrypted_payload"]
                                    .encode("ascii")
                                )
                            else:
                                yield msg
                        except (json.JSONDecodeError, TransportError) as exc:
                            logger.warning(
                                "dropping malformed stream event (%d bytes): %s",
                                len(event_data),
                                exc,
                            )
                        event_data = ""

    @asynccontextmanager
    async def connect(self):
        """Context manager that yields ``(read_stream, write_stream)`` callables.

        Provides a familiar stream-pair interface compatible with MCP client
        patterns.  ``write_stream`` sends a JSON-RPC message (encrypted);
        ``read_stream`` returns the next response or server-pushed message.

        Yields:
            A ``(read, write)`` tuple where ``read`` is an async callable
            returning a dict and ``write`` is an async callable accepting
            a dict.

        Example::

            async with transport.connect() as (read, write):
                await write({"jsonrpc": "2.0", "method": "initialize", "id": 1})
                response = await read()
        """
        response_queue: asyncio.Queue[Dict[str, Any]] = asyncio.Queue()

        async def write(message: Dict[str, Any]) -> None:
            """Encrypt and send a JSON-RPC message, queueing any response."""
            resp = await self.send(message)
            if resp is not None:
                await response_queue.put(resp)

        async def read() -> Dict[str, Any]:
            """Return the next decrypted message from the server."""
            return await response_queue.get()

        try:
            yield read, write
        finally:
            await self.close()

    async def close(self) -> None:
        """Close the transport and release resources.

        After closing, any subsequent :meth:`send`, :meth:`open_stream`,
        or :meth:`connect` call will raise :class:`RuntimeError`.

        If a session ID is active, sends a DELETE request to the server
        to terminate the session.

        Example::

            await transport.close()
        """
        if self._closed:
            return
        self._closed = True

        # Attempt to terminate the server session.
        if self._session_id:
            try:
                import httpx

                headers = dict(self._extra_headers)
                if self._tokens and not self._tokens.expired:
                    headers["Authorization"] = (
                        f"Bearer {self._tokens.access_token}"
                    )
                headers["Mcp-Session-Id"] = self._session_id

                async with httpx.AsyncClient(timeout=5.0) as client:
                    await client.delete(self._url, headers=headers)
            except Exception:
                logger.debug(
                    "failed to send session termination for %s",
                    self._session_id,
                )

        self._session_id = None
        logger.debug("Streamable HTTP transport closed for %s", self._url)


# ---------------------------------------------------------------------------
# Original encrypted channel transport (unchanged public API)
# ---------------------------------------------------------------------------

class SkytaleTransport:
    """MCP JSON-RPC transport over an MLS-encrypted Skytale channel.

    Replaces plaintext HTTP/stdio with end-to-end encrypted messaging.
    Each JSON-RPC message is wrapped in an :class:`Envelope` with
    :attr:`Protocol.MCP`.

    Can be used as a drop-in transport for MCP clients and servers
    that support custom transports.

    Args:
        manager: A :class:`SkytaleChannelManager` with the channel already
            created or joined.
        channel: Channel name in ``org/namespace/service`` format.

    Example::

        mgr = SkytaleChannelManager(identity=b"agent-1")
        mgr.create("org/ns/mcp-rpc")
        transport = SkytaleTransport(mgr, "org/ns/mcp-rpc")

        # In an async context:
        await transport.write({"jsonrpc": "2.0", "method": "ping", "id": 1})
        msg = await transport.read()
        await transport.close()
    """

    def __init__(self, manager: SkytaleChannelManager, channel: str) -> None:
        self._manager = manager
        self._channel = channel
        self._closed = False
        # Buffer for MCP messages that arrived in a batch but haven't been
        # consumed yet — avoids dropping messages when receive_envelopes
        # returns multiple envelopes at once.
        self._pending: List[Dict] = []

    async def read(self) -> Dict:
        """Read the next MCP JSON-RPC message from the encrypted channel.

        Polls the underlying channel for :attr:`Protocol.MCP` envelopes,
        deserializes the first one's payload as JSON, and returns it.  If
        multiple MCP envelopes arrive in a single poll, the extras are queued
        internally and returned by subsequent ``read()`` calls.

        Returns:
            A ``dict`` representing the JSON-RPC message.

        Raises:
            RuntimeError: If the transport has been closed.

        Example::

            msg = await transport.read()
            print(msg["method"])  # e.g. "tools/list"
        """
        if self._closed:
            raise RuntimeError("transport closed")

        # Drain the internal queue first before hitting the channel.
        if self._pending:
            return self._pending.pop(0)

        loop = asyncio.get_event_loop()

        while True:
            if self._closed:
                raise RuntimeError("transport closed")

            # receive_envelopes is synchronous — run in executor to avoid
            # blocking the event loop.
            envelopes: List[Envelope] = await loop.run_in_executor(
                None,
                self._manager.receive_envelopes,
                self._channel,
                1.0,
            )

            # Filter to MCP-protocol envelopes only; other protocols sharing
            # the same channel are silently ignored.
            mcp_messages: List[Dict] = []
            for env in envelopes:
                if env.protocol is not Protocol.MCP:
                    logger.debug(
                        "ignoring non-MCP envelope (protocol=%s) on channel %s",
                        env.protocol.value,
                        self._channel,
                    )
                    continue
                try:
                    mcp_messages.append(json.loads(env.payload))
                except json.JSONDecodeError:
                    logger.warning(
                        "dropping MCP envelope with invalid JSON payload "
                        "(%d bytes) on channel %s",
                        len(env.payload),
                        self._channel,
                    )

            if mcp_messages:
                # Return the first message now; stash the rest for later reads.
                self._pending.extend(mcp_messages[1:])
                return mcp_messages[0]

            # No MCP messages this round — loop and poll again.

    async def write(self, message: Dict) -> None:
        """Write an MCP JSON-RPC message to the encrypted channel.

        Serializes *message* to JSON bytes, wraps it in an :class:`Envelope`
        tagged with :attr:`Protocol.MCP`, and sends it through the channel.

        Args:
            message: A JSON-serializable ``dict`` (typically a JSON-RPC
                request, response, or notification).

        Raises:
            RuntimeError: If the transport has been closed.

        Example::

            await transport.write({
                "jsonrpc": "2.0",
                "method": "tools/list",
                "id": 1,
            })
        """
        if self._closed:
            raise RuntimeError("transport closed")

        payload = json.dumps(message, separators=(",", ":")).encode("utf-8")
        envelope = Envelope(
            protocol=Protocol.MCP,
            content_type="application/json",
            payload=payload,
        )

        loop = asyncio.get_event_loop()
        await loop.run_in_executor(
            None,
            self._manager.send_envelope,
            self._channel,
            envelope,
        )

        logger.debug(
            "wrote MCP message (%d bytes payload) to channel %s",
            len(payload),
            self._channel,
        )

    async def close(self) -> None:
        """Close the transport.

        After closing, any subsequent :meth:`read` or :meth:`write` call will
        raise :class:`RuntimeError`.  Pending buffered messages are discarded.

        Example::

            await transport.close()
        """
        self._closed = True
        self._pending.clear()
        logger.debug("transport closed for channel %s", self._channel)
