//! RPC error types.

/// Status codes for RPC responses.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum RpcStatus {
    /// Request succeeded.
    Ok = 0,
    /// Client sent a malformed request.
    BadRequest = 1,
    /// Authentication required or token invalid.
    Unauthorized = 2,
    /// Caller lacks permission for this operation.
    Forbidden = 3,
    /// Requested resource not found.
    NotFound = 4,
    /// Rate limit exceeded.
    RateLimited = 5,
    /// Request deadline exceeded (server-side timeout).
    DeadlineExceeded = 8,
    /// Server is shutting down (draining).
    Unavailable = 9,
    /// Internal server error.
    Internal = 10,
    /// Method not recognized.
    UnknownMethod = 11,
}

impl RpcStatus {
    /// Decode a status byte. Returns `None` for unknown values.
    pub fn from_u8(v: u8) -> Option<Self> {
        match v {
            0 => Some(Self::Ok),
            1 => Some(Self::BadRequest),
            2 => Some(Self::Unauthorized),
            3 => Some(Self::Forbidden),
            4 => Some(Self::NotFound),
            5 => Some(Self::RateLimited),
            8 => Some(Self::DeadlineExceeded),
            9 => Some(Self::Unavailable),
            10 => Some(Self::Internal),
            11 => Some(Self::UnknownMethod),
            _ => None,
        }
    }
}

/// Errors that can occur in the RPC layer.
#[derive(Debug, thiserror::Error)]
pub enum RpcError {
    #[error("connection error: {0}")]
    Connection(String),

    #[error("encoding error: {0}")]
    Encode(String),

    #[error("decoding error: {0}")]
    Decode(String),

    #[error("server returned error status {status:?}: {message}")]
    Server {
        status: RpcStatus,
        message: String,
    },

    #[error("request timed out")]
    Timeout,

    #[error("stream closed unexpectedly")]
    StreamClosed,

    #[error("payload too large: {size} bytes (max {max})")]
    PayloadTooLarge { size: usize, max: usize },
}

impl RpcError {
    /// Returns `true` if this error is transient and the operation may succeed
    /// on retry (e.g. connection reset, timeout, server 5xx). Returns `false`
    /// for permanent failures (auth, bad request, payload limits).
    pub fn is_retriable(&self) -> bool {
        match self {
            Self::Connection(_) | Self::Timeout | Self::StreamClosed => true,
            Self::Server { status, .. } => matches!(
                status,
                RpcStatus::Unavailable
                    | RpcStatus::DeadlineExceeded
                    | RpcStatus::Internal
                    | RpcStatus::RateLimited
            ),
            Self::Encode(_) | Self::Decode(_) | Self::PayloadTooLarge { .. } => false,
        }
    }
}

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

    #[test]
    fn retriable_errors() {
        assert!(RpcError::Connection("reset".into()).is_retriable());
        assert!(RpcError::Timeout.is_retriable());
        assert!(RpcError::StreamClosed.is_retriable());
        assert!(RpcError::Server {
            status: RpcStatus::Unavailable,
            message: String::new(),
        }
        .is_retriable());
        assert!(RpcError::Server {
            status: RpcStatus::Internal,
            message: String::new(),
        }
        .is_retriable());
        assert!(RpcError::Server {
            status: RpcStatus::DeadlineExceeded,
            message: String::new(),
        }
        .is_retriable());
        assert!(RpcError::Server {
            status: RpcStatus::RateLimited,
            message: String::new(),
        }
        .is_retriable());
    }

    #[test]
    fn non_retriable_errors() {
        assert!(!RpcError::Encode("bad".into()).is_retriable());
        assert!(!RpcError::Decode("bad".into()).is_retriable());
        assert!(!RpcError::PayloadTooLarge { size: 100, max: 50 }.is_retriable());
        assert!(!RpcError::Server {
            status: RpcStatus::Unauthorized,
            message: String::new(),
        }
        .is_retriable());
        assert!(!RpcError::Server {
            status: RpcStatus::BadRequest,
            message: String::new(),
        }
        .is_retriable());
        assert!(!RpcError::Server {
            status: RpcStatus::Forbidden,
            message: String::new(),
        }
        .is_retriable());
        assert!(!RpcError::Server {
            status: RpcStatus::NotFound,
            message: String::new(),
        }
        .is_retriable());
    }

    #[test]
    fn connection_state_display() {
        use crate::client::ConnectionState;
        assert_eq!(ConnectionState::Connected.to_string(), "Connected");
        assert_eq!(ConnectionState::Disconnected.to_string(), "Disconnected");
        assert_eq!(
            ConnectionState::Reconnecting { attempt: 2 }.to_string(),
            "Reconnecting (attempt 2)"
        );
    }
}
