//! Minimal transport abstraction the protocol client drives.
//!
//! ADR-0002 rejected a general-purpose `Transport` trait for the engine,
//! preferring sibling modules under `transport::*`. The trait here is
//! intentionally narrower: it expresses only the contract the protocol
//! [`Client`](super::Client) cares about (one outbound channel of
//! envelopes, one inbound channel of envelopes). Concrete transports
//! own a background task that bridges OS-level I/O to those channels,
//! and they expose their native API to callers that want it.
//!
//! We keep the trait off this surface entirely. Using channels for the
//! contract sidesteps async-in-trait (no proc-macro crate, no dyn
//! dispatch over async fn), keeps tests obvious (a mock is two channels
//! plumbed into the Client), and matches what real transports already
//! produce internally. See // for the concrete
//! transport impls.
//!
//! WOR-1313 added bounded variants: a transport that talks to an
//! untrusted peer (stdio today) hands the Client bounded channel ends so
//! a flooding server cannot grow process memory without limit. The
//! unbounded constructor stays for transports whose inbound volume is
//! request-paced (HTTP, replay) and for test mocks.

use thiserror::Error;
use tokio::sync::mpsc;

use super::JsonRpcMessage;

/// A transport channel refused an envelope.
///
/// Exists so callers can keep one error shape across the bounded and
/// unbounded channel variants: `closed` means the peer half is gone,
/// `full` means a bounded channel rejected a non-awaiting send.
#[derive(Debug, Error)]
#[error("transport channel rejected the envelope: {reason}")]
pub struct TransportSendError {
    /// The envelope that was not delivered, returned so callers can
    /// retry or log it instead of losing the payload. Boxed to keep
    /// the `Result` small on the hot send path (clippy
    /// `result_large_err`).
    pub message: Box<JsonRpcMessage>,
    /// Why the channel refused: `"closed"` or `"full"`.
    pub reason: &'static str,
}

/// Sending half of a [`Transport`], bounded or unbounded.
///
/// An enum (not a trait object) so the Client and the concrete
/// transports keep static dispatch and `Clone` stays a cheap channel
/// clone. Callers in async contexts should prefer [`Self::send_wait`],
/// which applies backpressure on the bounded variant instead of
/// failing on a full queue.
#[derive(Debug, Clone)]
pub enum TransportSender {
    /// Classic unbounded sender; `send` never blocks and never fills.
    Unbounded(mpsc::UnboundedSender<JsonRpcMessage>),
    /// Bounded sender (WOR-1313); full queues push back on the caller.
    Bounded(mpsc::Sender<JsonRpcMessage>),
}

impl TransportSender {
    /// Non-awaiting send. On the bounded variant this is a `try_send`,
    /// so a full queue is an error; use [`Self::send_wait`] from async
    /// code that can afford to wait.
    pub fn send(&self, message: JsonRpcMessage) -> Result<(), TransportSendError> {
        match self {
            Self::Unbounded(tx) => tx.send(message).map_err(|e| TransportSendError {
                message: Box::new(e.0),
                reason: "closed",
            }),
            Self::Bounded(tx) => tx.try_send(message).map_err(|e| match e {
                mpsc::error::TrySendError::Full(m) => TransportSendError {
                    message: Box::new(m),
                    reason: "full",
                },
                mpsc::error::TrySendError::Closed(m) => TransportSendError {
                    message: Box::new(m),
                    reason: "closed",
                },
            }),
        }
    }

    /// Awaiting send: backpressure on the bounded variant, immediate on
    /// the unbounded one. The only error left is a closed channel.
    pub async fn send_wait(&self, message: JsonRpcMessage) -> Result<(), TransportSendError> {
        match self {
            Self::Unbounded(tx) => tx.send(message).map_err(|e| TransportSendError {
                message: Box::new(e.0),
                reason: "closed",
            }),
            Self::Bounded(tx) => tx.send(message).await.map_err(|e| TransportSendError {
                message: Box::new(e.0),
                reason: "closed",
            }),
        }
    }
}

/// Receiving half of a [`Transport`], bounded or unbounded.
///
/// Mirrors [`TransportSender`]; `recv` has the same shape on both
/// variants so consumers (the Client pump, integration tests) do not
/// care which kind of channel the transport chose.
#[derive(Debug)]
pub enum TransportReceiver {
    /// Receiver for the classic unbounded channel.
    Unbounded(mpsc::UnboundedReceiver<JsonRpcMessage>),
    /// Receiver for the bounded channel (WOR-1313).
    Bounded(mpsc::Receiver<JsonRpcMessage>),
}

impl TransportReceiver {
    /// Receive the next envelope; `None` once the channel is closed and
    /// drained, same contract as the raw tokio receivers.
    pub async fn recv(&mut self) -> Option<JsonRpcMessage> {
        match self {
            Self::Unbounded(rx) => rx.recv().await,
            Self::Bounded(rx) => rx.recv().await,
        }
    }
}

/// Channel pair the protocol [`Client`](super::Client) consumes.
///
/// `outbound_tx` is held by the Client; the transport's writer task
/// drains it and frames messages for the wire. `inbound_rx` is held by
/// the Client; the transport's reader task pushes parsed envelopes
/// through it. Closing either channel signals shutdown to the other
/// half, which is the same shape every concrete transport uses.
#[derive(Debug)]
pub struct Transport {
    /// Outbound queue: the Client sends here, the transport drains it.
    pub outbound_tx: TransportSender,
    /// Inbound queue: the transport pushes here, the Client drains it.
    pub inbound_rx: TransportReceiver,
}

impl Transport {
    /// Wire a pair of unbounded channels into a [`Transport`].
    pub fn new(
        outbound_tx: mpsc::UnboundedSender<JsonRpcMessage>,
        inbound_rx: mpsc::UnboundedReceiver<JsonRpcMessage>,
    ) -> Self {
        Self {
            outbound_tx: TransportSender::Unbounded(outbound_tx),
            inbound_rx: TransportReceiver::Unbounded(inbound_rx),
        }
    }

    /// Wire a pair of bounded channels into a [`Transport`] (WOR-1313).
    ///
    /// For transports facing an untrusted peer: a flooding server fills
    /// the inbound queue instead of growing process memory, and the
    /// transport's reader can detect the sustained full state.
    pub fn new_bounded(
        outbound_tx: mpsc::Sender<JsonRpcMessage>,
        inbound_rx: mpsc::Receiver<JsonRpcMessage>,
    ) -> Self {
        Self {
            outbound_tx: TransportSender::Bounded(outbound_tx),
            inbound_rx: TransportReceiver::Bounded(inbound_rx),
        }
    }
}
