//! Typed errors raised by the protocol [`Client`](super::Client).
//!
//! Why these live in their own module: every public protocol op returns
//! one of these variants, and growing the surface in one place keeps
//! match arms in callers honest.

use thiserror::Error;

/// Errors surfaced by [`Client`](super::Client) operations.
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ProtocolError {
    /// The server proposed a protocol version we do not support.
    #[error("server proposed unsupported protocol version `{0}` (supported: 2024-11-05, 2025-03-26, 2025-06-18, 2026-03-26)")]
    ProtocolVersionMismatch(String),

    /// The server returned a message that does not match the JSON-RPC
    /// envelope shape (missing `jsonrpc`, both `result` and `error`,
    /// non-string method on a request, etc.).
    #[error("malformed JSON-RPC message: {0}")]
    MalformedMessage(String),

    /// A request did not receive a response within the configured
    /// timeout. The request ID is included so callers can correlate
    /// with their own telemetry.
    #[error(
        "request {id} timed out after {timeout_ms}ms{}",
        render_timeout_hint(hint)
    )]
    RequestTimeout {
        /// Numeric request ID handed to the transport.
        id: i64,
        /// Configured timeout in milliseconds.
        timeout_ms: u64,
        /// Diagnosis hint attached by the client (WOR-1311): when the
        /// connection recorded protocol violations, the timeout is
        /// usually their fault, and the hint says so instead of leaving
        /// the operator to suspect their own timeout setting. Build via
        /// [`ProtocolError::request_timeout`] or
        /// [`ProtocolError::request_timeout_with_hint`].
        hint: Option<String>,
    },

    /// The server returned a JSON-RPC error response. `code` and
    /// `message` are passed through verbatim; `data` is optional.
    #[error("server returned JSON-RPC error {code}: {message}")]
    ServerError {
        /// JSON-RPC error code, as defined by the spec.
        code: i64,
        /// Free-text human-readable message.
        message: String,
        /// Optional structured data payload.
        data: Option<serde_json::Value>,
    },

    /// The underlying transport closed before the operation completed.
    #[error("transport closed: {0}")]
    TransportClosed(String),

    /// The Client was shut down while this request was in flight.
    #[error("client shut down while request {id} was in flight")]
    Aborted {
        /// Numeric request ID handed to the transport.
        id: i64,
    },

    /// A JSON (de)serialization error escaping from the wire layer.
    #[error("json error: {0}")]
    Json(#[from] serde_json::Error),

    /// Catch-all transport I/O surface. Carries an owned string so the
    /// underlying error type (channel send error, transport-specific
    /// error) stays opaque to callers.
    #[error("transport error: {0}")]
    Transport(String),
}

impl ProtocolError {
    /// Build a [`ProtocolError::RequestTimeout`] with no hint.
    ///
    /// Exists so call sites that have no violation evidence (tests,
    /// classifiers) do not have to spell out the `hint` field that
    /// WOR-1311 added to the variant.
    pub fn request_timeout(id: i64, timeout_ms: u64) -> Self {
        Self::RequestTimeout {
            id,
            timeout_ms,
            hint: None,
        }
    }

    /// Build a [`ProtocolError::RequestTimeout`] carrying an optional
    /// diagnosis hint. The client passes the connection's
    /// protocol-violation summary here so Display can append
    /// `(note: <hint>)` (WOR-1311).
    pub fn request_timeout_with_hint(id: i64, timeout_ms: u64, hint: Option<String>) -> Self {
        Self::RequestTimeout {
            id,
            timeout_ms,
            hint,
        }
    }
}

/// Render the optional timeout hint as a Display suffix: empty when
/// absent, ` (note: <hint>)` when present. A helper rather than inline
/// formatting because `thiserror` cannot conditionally format an
/// `Option` field inside the error attribute.
fn render_timeout_hint(hint: &Option<String>) -> String {
    match hint {
        Some(hint) => format!(" (note: {hint})"),
        None => String::new(),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn request_timeout_display_is_bare_without_a_hint() {
        let err = ProtocolError::request_timeout(7, 5000);
        assert_eq!(err.to_string(), "request 7 timed out after 5000ms");
    }

    #[test]
    fn request_timeout_display_appends_the_note_when_hinted() {
        let err = ProtocolError::request_timeout_with_hint(
            7,
            5000,
            Some("2 protocol violations observed on this connection".into()),
        );
        assert_eq!(
            err.to_string(),
            "request 7 timed out after 5000ms \
             (note: 2 protocol violations observed on this connection)"
        );
    }
}
