use crate::body::BoxBody;
use crate::metadata::MetadataMap;
use bytes::Bytes;
use http::header::{HeaderMap, HeaderValue};
use percent_encoding::{percent_decode, percent_encode, AsciiSet, CONTROLS};
use std::{borrow::Cow, error::Error, fmt, sync::Arc};
use tracing::{debug, trace, warn};

const ENCODING_SET: &AsciiSet = &CONTROLS
    .add(b' ')
    .add(b'"')
    .add(b'#')
    .add(b'<')
    .add(b'>')
    .add(b'`')
    .add(b'?')
    .add(b'{')
    .add(b'}');

const GRPC_STATUS_HEADER_CODE: &str = "grpc-status";
const GRPC_STATUS_MESSAGE_HEADER: &str = "grpc-message";
const GRPC_STATUS_DETAILS_HEADER: &str = "grpc-status-details-bin";

/// A gRPC status describing the result of an RPC call.
///
/// Values can be created using the `new` function or one of the specialized
/// associated functions.
/// ```rust
/// # use tonic::{Status, Code};
/// let status1 = Status::new(Code::InvalidArgument, "name is invalid");
/// let status2 = Status::invalid_argument("name is invalid");
///
/// assert_eq!(status1.code(), Code::InvalidArgument);
/// assert_eq!(status1.code(), status2.code());
/// ```
#[derive(Clone)]
pub struct Status {
    /// The gRPC status code, found in the `grpc-status` header.
    code: Code,
    /// A relevant error message, found in the `grpc-message` header.
    message: String,
    /// Binary opaque details, found in the `grpc-status-details-bin` header.
    details: Bytes,
    /// Custom metadata, found in the user-defined headers.
    /// If the metadata contains any headers with names reserved either by the gRPC spec
    /// or by `Status` fields above, they will be ignored.
    metadata: MetadataMap,
    /// Optional underlying error.
    source: Option<Arc<dyn Error + Send + Sync + 'static>>,
}

/// gRPC status codes used by [`Status`].
///
/// These variants match the [gRPC status codes].
///
/// [gRPC status codes]: https://github.com/grpc/grpc/blob/master/doc/statuscodes.md#status-codes-and-their-use-in-grpc
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Code {
    /// The operation completed successfully.
    Ok = 0,

    /// The operation was cancelled.
    Cancelled = 1,

    /// Unknown error.
    Unknown = 2,

    /// Client specified an invalid argument.
    InvalidArgument = 3,

    /// Deadline expired before operation could complete.
    DeadlineExceeded = 4,

    /// Some requested entity was not found.
    NotFound = 5,

    /// Some entity that we attempted to create already exists.
    AlreadyExists = 6,

    /// The caller does not have permission to execute the specified operation.
    PermissionDenied = 7,

    /// Some resource has been exhausted.
    ResourceExhausted = 8,

    /// The system is not in a state required for the operation's execution.
    FailedPrecondition = 9,

    /// The operation was aborted.
    Aborted = 10,

    /// Operation was attempted past the valid range.
    OutOfRange = 11,

    /// Operation is not implemented or not supported.
    Unimplemented = 12,

    /// Internal error.
    Internal = 13,

    /// The service is currently unavailable.
    Unavailable = 14,

    /// Unrecoverable data loss or corruption.
    DataLoss = 15,

    /// The request does not have valid authentication credentials
    Unauthenticated = 16,
}

impl Code {
    /// Get description of this `Code`.
    /// ```
    /// fn make_grpc_request() -> tonic::Code {
    ///     // ...
    ///     tonic::Code::Ok
    /// }
    /// let code = make_grpc_request();
    /// println!("Operation completed. Human readable description: {}", code.description());
    /// ```
    /// If you only need description in `println`, `format`, `log` and other
    /// formatting contexts, you may want to use `Display` impl for `Code`
    /// instead.
    pub fn description(&self) -> &'static str {
        match self {
            Code::Ok => "The operation completed successfully",
            Code::Cancelled => "The operation was cancelled",
            Code::Unknown => "Unknown error",
            Code::InvalidArgument => "Client specified an invalid argument",
            Code::DeadlineExceeded => "Deadline expired before operation could complete",
            Code::NotFound => "Some requested entity was not found",
            Code::AlreadyExists => "Some entity that we attempted to create already exists",
            Code::PermissionDenied => {
                "The caller does not have permission to execute the specified operation"
            }
            Code::ResourceExhausted => "Some resource has been exhausted",
            Code::FailedPrecondition => {
                "The system is not in a state required for the operation's execution"
            }
            Code::Aborted => "The operation was aborted",
            Code::OutOfRange => "Operation was attempted past the valid range",
            Code::Unimplemented => "Operation is not implemented or not supported",
            Code::Internal => "Internal error",
            Code::Unavailable => "The service is currently unavailable",
            Code::DataLoss => "Unrecoverable data loss or corruption",
            Code::Unauthenticated => "The request does not have valid authentication credentials",
        }
    }
}

impl std::fmt::Display for Code {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        std::fmt::Display::fmt(self.description(), f)
    }
}

// ===== impl Status =====

impl Status {
    /// Create a new `Status` with the associated code and message.
    pub fn new(code: Code, message: impl Into<String>) -> Status {
        Status {
            code,
            message: message.into(),
            details: Bytes::new(),
            metadata: MetadataMap::new(),
            source: None,
        }
    }

    /// The operation completed successfully.
    pub fn ok(message: impl Into<String>) -> Status {
        Status::new(Code::Ok, message)
    }

    /// The operation was cancelled (typically by the caller).
    pub fn cancelled(message: impl Into<String>) -> Status {
        Status::new(Code::Cancelled, message)
    }

    /// Unknown error. An example of where this error may be returned is if a
    /// `Status` value received from another address space belongs to an error-space
    /// that is not known in this address space. Also errors raised by APIs that
    /// do not return enough error information may be converted to this error.
    pub fn unknown(message: impl Into<String>) -> Status {
        Status::new(Code::Unknown, message)
    }

    /// Client specified an invalid argument. Note that this differs from
    /// `FailedPrecondition`. `InvalidArgument` indicates arguments that are
    /// problematic regardless of the state of the system (e.g., a malformed file
    /// name).
    pub fn invalid_argument(message: impl Into<String>) -> Status {
        Status::new(Code::InvalidArgument, message)
    }

    /// Deadline expired before operation could complete. For operations that
    /// change the state of the system, this error may be returned even if the
    /// operation has completed successfully. For example, a successful response
    /// from a server could have been delayed long enough for the deadline to
    /// expire.
    pub fn deadline_exceeded(message: impl Into<String>) -> Status {
        Status::new(Code::DeadlineExceeded, message)
    }

    /// Some requested entity (e.g., file or directory) was not found.
    pub fn not_found(message: impl Into<String>) -> Status {
        Status::new(Code::NotFound, message)
    }

    /// Some entity that we attempted to create (e.g., file or directory) already
    /// exists.
    pub fn already_exists(message: impl Into<String>) -> Status {
        Status::new(Code::AlreadyExists, message)
    }

    /// The caller does not have permission to execute the specified operation.
    /// `PermissionDenied` must not be used for rejections caused by exhausting
    /// some resource (use `ResourceExhausted` instead for those errors).
    /// `PermissionDenied` must not be used if the caller cannot be identified
    /// (use `Unauthenticated` instead for those errors).
    pub fn permission_denied(message: impl Into<String>) -> Status {
        Status::new(Code::PermissionDenied, message)
    }

    /// Some resource has been exhausted, perhaps a per-user quota, or perhaps
    /// the entire file system is out of space.
    pub fn resource_exhausted(message: impl Into<String>) -> Status {
        Status::new(Code::ResourceExhausted, message)
    }

    /// Operation was rejected because the system is not in a state required for
    /// the operation's execution. For example, directory to be deleted may be
    /// non-empty, an rmdir operation is applied to a non-directory, etc.
    ///
    /// A litmus test that may help a service implementor in deciding between
    /// `FailedPrecondition`, `Aborted`, and `Unavailable`:
    /// (a) Use `Unavailable` if the client can retry just the failing call.
    /// (b) Use `Aborted` if the client should retry at a higher-level (e.g.,
    ///     restarting a read-modify-write sequence).
    /// (c) Use `FailedPrecondition` if the client should not retry until the
    ///     system state has been explicitly fixed.  E.g., if an "rmdir" fails
    ///     because the directory is non-empty, `FailedPrecondition` should be
    ///     returned since the client should not retry unless they have first
    ///     fixed up the directory by deleting files from it.
    pub fn failed_precondition(message: impl Into<String>) -> Status {
        Status::new(Code::FailedPrecondition, message)
    }

    /// The operation was aborted, typically due to a concurrency issue like
    /// sequencer check failures, transaction aborts, etc.
    ///
    /// See litmus test above for deciding between `FailedPrecondition`,
    /// `Aborted`, and `Unavailable`.
    pub fn aborted(message: impl Into<String>) -> Status {
        Status::new(Code::Aborted, message)
    }

    /// Operation was attempted past the valid range. E.g., seeking or reading
    /// past end of file.
    ///
    /// Unlike `InvalidArgument`, this error indicates a problem that may be
    /// fixed if the system state changes. For example, a 32-bit file system will
    /// generate `InvalidArgument if asked to read at an offset that is not in the
    /// range [0,2^32-1], but it will generate `OutOfRange` if asked to read from
    /// an offset past the current file size.
    ///
    /// There is a fair bit of overlap between `FailedPrecondition` and
    /// `OutOfRange`. We recommend using `OutOfRange` (the more specific error)
    /// when it applies so that callers who are iterating through a space can
    /// easily look for an `OutOfRange` error to detect when they are done.
    pub fn out_of_range(message: impl Into<String>) -> Status {
        Status::new(Code::OutOfRange, message)
    }

    /// Operation is not implemented or not supported/enabled in this service.
    pub fn unimplemented(message: impl Into<String>) -> Status {
        Status::new(Code::Unimplemented, message)
    }

    /// Internal errors. Means some invariants expected by underlying system has
    /// been broken. If you see one of these errors, something is very broken.
    pub fn internal(message: impl Into<String>) -> Status {
        Status::new(Code::Internal, message)
    }

    /// The service is currently unavailable.  This is a most likely a transient
    /// condition and may be corrected by retrying with a back-off.
    ///
    /// See litmus test above for deciding between `FailedPrecondition`,
    /// `Aborted`, and `Unavailable`.
    pub fn unavailable(message: impl Into<String>) -> Status {
        Status::new(Code::Unavailable, message)
    }

    /// Unrecoverable data loss or corruption.
    pub fn data_loss(message: impl Into<String>) -> Status {
        Status::new(Code::DataLoss, message)
    }

    /// The request does not have valid authentication credentials for the
    /// operation.
    pub fn unauthenticated(message: impl Into<String>) -> Status {
        Status::new(Code::Unauthenticated, message)
    }

    #[cfg_attr(not(feature = "transport"), allow(dead_code))]
    pub(crate) fn from_error_generic(
        err: impl Into<Box<dyn Error + Send + Sync + 'static>>,
    ) -> Status {
        Self::from_error(err.into())
    }

    /// Create a `Status` from various types of `Error`.
    ///
    /// Inspects the error source chain for recognizable errors, including statuses, HTTP2, and
    /// hyper, and attempts to maps them to a `Status`, or else returns an Unknown `Status`.
    #[cfg_attr(not(feature = "transport"), allow(dead_code))]
    pub fn from_error(err: Box<dyn Error + Send + Sync + 'static>) -> Status {
        Status::try_from_error(err).unwrap_or_else(|err| {
            let mut status = Status::new(Code::Unknown, err.to_string());
            status.source = Some(err.into());
            status
        })
    }

    pub(crate) fn try_from_error(
        err: Box<dyn Error + Send + Sync + 'static>,
    ) -> Result<Status, Box<dyn Error + Send + Sync + 'static>> {
        let err = match err.downcast::<Status>() {
            Ok(status) => {
                return Ok(*status);
            }
            Err(err) => err,
        };

        #[cfg(feature = "transport")]
        let err = match err.downcast::<h2::Error>() {
            Ok(h2) => {
                return Ok(Status::from_h2_error(h2));
            }
            Err(err) => err,
        };

        if let Some(mut status) = find_status_in_source_chain(&*err) {
            status.source = Some(err.into());
            return Ok(status);
        }

        Err(err)
    }

    // FIXME: bubble this into `transport` and expose generic http2 reasons.
    #[cfg(feature = "transport")]
    fn from_h2_error(err: Box<h2::Error>) -> Status {
        // See https://github.com/grpc/grpc/blob/3977c30/doc/PROTOCOL-HTTP2.md#errors
        let code = match err.reason() {
            Some(h2::Reason::NO_ERROR)
            | Some(h2::Reason::PROTOCOL_ERROR)
            | Some(h2::Reason::INTERNAL_ERROR)
            | Some(h2::Reason::FLOW_CONTROL_ERROR)
            | Some(h2::Reason::SETTINGS_TIMEOUT)
            | Some(h2::Reason::COMPRESSION_ERROR)
            | Some(h2::Reason::CONNECT_ERROR) => Code::Internal,
            Some(h2::Reason::REFUSED_STREAM) => Code::Unavailable,
            Some(h2::Reason::CANCEL) => Code::Cancelled,
            Some(h2::Reason::ENHANCE_YOUR_CALM) => Code::ResourceExhausted,
            Some(h2::Reason::INADEQUATE_SECURITY) => Code::PermissionDenied,

            _ => Code::Unknown,
        };

        let mut status = Self::new(code, format!("h2 protocol error: {}", err));
        status.source = Some(Arc::new(*err));
        status
    }

    #[cfg(feature = "transport")]
    fn to_h2_error(&self) -> h2::Error {
        // conservatively transform to h2 error codes...
        let reason = match self.code {
            Code::Cancelled => h2::Reason::CANCEL,
            _ => h2::Reason::INTERNAL_ERROR,
        };

        reason.into()
    }

    /// Handles hyper errors specifically, which expose a number of different parameters about the
    /// http stream's error: https://docs.rs/hyper/0.14.11/hyper/struct.Error.html.
    ///
    /// Returns Some if there's a way to handle the error, or None if the information from this
    /// hyper error, but perhaps not its source, should be ignored.
    #[cfg(feature = "transport")]
    fn from_hyper_error(err: &hyper::Error) -> Option<Status> {
        // is_timeout results from hyper's keep-alive logic
        // (https://docs.rs/hyper/0.14.11/src/hyper/error.rs.html#192-194).  Per the grpc spec
        // > An expired client initiated PING will cause all calls to be closed with an UNAVAILABLE
        // > status. Note that the frequency of PINGs is highly dependent on the network
        // > environment, implementations are free to adjust PING frequency based on network and
        // > application requirements, which is why it's mapped to unavailable here.
        //
        // Likewise, if we are unable to connect to the server, map this to UNAVAILABLE.  This is
        // consistent with the behavior of a C++ gRPC client when the server is not running, and
        // matches the spec of:
        // > The service is currently unavailable. This is most likely a transient condition that
        // > can be corrected if retried with a backoff.
        if err.is_timeout() || err.is_connect() {
            return Some(Status::unavailable(err.to_string()));
        }
        None
    }

    pub(crate) fn map_error<E>(err: E) -> Status
    where
        E: Into<Box<dyn Error + Send + Sync>>,
    {
        let err: Box<dyn Error + Send + Sync> = err.into();
        Status::from_error(err)
    }

    /// Extract a `Status` from a hyper `HeaderMap`.
    pub fn from_header_map(header_map: &HeaderMap) -> Option<Status> {
        header_map.get(GRPC_STATUS_HEADER_CODE).map(|code| {
            let code = Code::from_bytes(code.as_ref());
            let error_message = header_map
                .get(GRPC_STATUS_MESSAGE_HEADER)
                .map(|header| {
                    percent_decode(header.as_bytes())
                        .decode_utf8()
                        .map(|cow| cow.to_string())
                })
                .unwrap_or_else(|| Ok(String::new()));

            let details = header_map
                .get(GRPC_STATUS_DETAILS_HEADER)
                .map(|h| {
                    base64::decode(h.as_bytes())
                        .expect("Invalid status header, expected base64 encoded value")
                })
                .map(Bytes::from)
                .unwrap_or_else(Bytes::new);

            let mut other_headers = header_map.clone();
            other_headers.remove(GRPC_STATUS_HEADER_CODE);
            other_headers.remove(GRPC_STATUS_MESSAGE_HEADER);
            other_headers.remove(GRPC_STATUS_DETAILS_HEADER);

            match error_message {
                Ok(message) => Status {
                    code,
                    message,
                    details,
                    metadata: MetadataMap::from_headers(other_headers),
                    source: None,
                },
                Err(err) => {
                    warn!("Error deserializing status message header: {}", err);
                    Status {
                        code: Code::Unknown,
                        message: format!("Error deserializing status message header: {}", err),
                        details,
                        metadata: MetadataMap::from_headers(other_headers),
                        source: None,
                    }
                }
            }
        })
    }

    /// Get the gRPC `Code` of this `Status`.
    pub fn code(&self) -> Code {
        self.code
    }

    /// Get the text error message of this `Status`.
    pub fn message(&self) -> &str {
        &self.message
    }

    /// Get the opaque error details of this `Status`.
    pub fn details(&self) -> &[u8] {
        &self.details
    }

    /// Get a reference to the custom metadata.
    pub fn metadata(&self) -> &MetadataMap {
        &self.metadata
    }

    /// Get a mutable reference to the custom metadata.
    pub fn metadata_mut(&mut self) -> &mut MetadataMap {
        &mut self.metadata
    }

    pub(crate) fn to_header_map(&self) -> Result<HeaderMap, Self> {
        let mut header_map = HeaderMap::with_capacity(3 + self.metadata.len());
        self.add_header(&mut header_map)?;
        Ok(header_map)
    }

    pub(crate) fn add_header(&self, header_map: &mut HeaderMap) -> Result<(), Self> {
        header_map.extend(self.metadata.clone().into_sanitized_headers());

        header_map.insert(GRPC_STATUS_HEADER_CODE, self.code.to_header_value());

        if !self.message.is_empty() {
            let to_write = Bytes::copy_from_slice(
                Cow::from(percent_encode(self.message().as_bytes(), ENCODING_SET)).as_bytes(),
            );

            header_map.insert(
                GRPC_STATUS_MESSAGE_HEADER,
                HeaderValue::from_maybe_shared(to_write).map_err(invalid_header_value_byte)?,
            );
        }

        if !self.details.is_empty() {
            let details = base64::encode_config(&self.details[..], base64::STANDARD_NO_PAD);

            header_map.insert(
                GRPC_STATUS_DETAILS_HEADER,
                HeaderValue::from_maybe_shared(details).map_err(invalid_header_value_byte)?,
            );
        }

        Ok(())
    }

    /// Create a new `Status` with the associated code, message, and binary details field.
    pub fn with_details(code: Code, message: impl Into<String>, details: Bytes) -> Status {
        Self::with_details_and_metadata(code, message, details, MetadataMap::new())
    }

    /// Create a new `Status` with the associated code, message, and custom metadata
    pub fn with_metadata(code: Code, message: impl Into<String>, metadata: MetadataMap) -> Status {
        Self::with_details_and_metadata(code, message, Bytes::new(), metadata)
    }

    /// Create a new `Status` with the associated code, message, binary details field and custom metadata
    pub fn with_details_and_metadata(
        code: Code,
        message: impl Into<String>,
        details: Bytes,
        metadata: MetadataMap,
    ) -> Status {
        Status {
            code,
            message: message.into(),
            details,
            metadata,
            source: None,
        }
    }

    #[allow(clippy::wrong_self_convention)]
    /// Build an `http::Response` from the given `Status`.
    pub fn to_http(self) -> http::Response<BoxBody> {
        let (mut parts, _body) = http::Response::new(()).into_parts();

        parts.headers.insert(
            http::header::CONTENT_TYPE,
            http::header::HeaderValue::from_static("application/grpc"),
        );

        self.add_header(&mut parts.headers).unwrap();

        http::Response::from_parts(parts, crate::body::empty_body())
    }
}

fn find_status_in_source_chain(err: &(dyn Error + 'static)) -> Option<Status> {
    let mut source = Some(err);

    while let Some(err) = source {
        if let Some(status) = err.downcast_ref::<Status>() {
            return Some(Status {
                code: status.code,
                message: status.message.clone(),
                details: status.details.clone(),
                metadata: status.metadata.clone(),
                // Since `Status` is not `Clone`, any `source` on the original Status
                // cannot be cloned so must remain with the original `Status`.
                source: None,
            });
        }

        #[cfg(feature = "transport")]
        if let Some(timeout) = err.downcast_ref::<crate::transport::TimeoutExpired>() {
            return Some(Status::cancelled(timeout.to_string()));
        }

        #[cfg(feature = "transport")]
        if let Some(hyper) = err
            .downcast_ref::<hyper::Error>()
            .and_then(Status::from_hyper_error)
        {
            return Some(hyper);
        }

        source = err.source();
    }

    None
}

impl fmt::Debug for Status {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        // A manual impl to reduce the noise of frequently empty fields.
        let mut builder = f.debug_struct("Status");

        builder.field("code", &self.code);

        if !self.message.is_empty() {
            builder.field("message", &self.message);
        }

        if !self.details.is_empty() {
            builder.field("details", &self.details);
        }

        if !self.metadata.is_empty() {
            builder.field("metadata", &self.metadata);
        }

        builder.field("source", &self.source);

        builder.finish()
    }
}

fn invalid_header_value_byte<Error: fmt::Display>(err: Error) -> Status {
    debug!("Invalid header: {}", err);
    Status::new(
        Code::Internal,
        "Couldn't serialize non-text grpc status header".to_string(),
    )
}

#[cfg(feature = "transport")]
impl From<h2::Error> for Status {
    fn from(err: h2::Error) -> Self {
        Status::from_h2_error(Box::new(err))
    }
}

#[cfg(feature = "transport")]
impl From<Status> for h2::Error {
    fn from(status: Status) -> Self {
        status.to_h2_error()
    }
}

impl From<std::io::Error> for Status {
    fn from(err: std::io::Error) -> Self {
        use std::io::ErrorKind;
        let code = match err.kind() {
            ErrorKind::BrokenPipe
            | ErrorKind::WouldBlock
            | ErrorKind::WriteZero
            | ErrorKind::Interrupted => Code::Internal,
            ErrorKind::ConnectionRefused
            | ErrorKind::ConnectionReset
            | ErrorKind::NotConnected
            | ErrorKind::AddrInUse
            | ErrorKind::AddrNotAvailable => Code::Unavailable,
            ErrorKind::AlreadyExists => Code::AlreadyExists,
            ErrorKind::ConnectionAborted => Code::Aborted,
            ErrorKind::InvalidData => Code::DataLoss,
            ErrorKind::InvalidInput => Code::InvalidArgument,
            ErrorKind::NotFound => Code::NotFound,
            ErrorKind::PermissionDenied => Code::PermissionDenied,
            ErrorKind::TimedOut => Code::DeadlineExceeded,
            ErrorKind::UnexpectedEof => Code::OutOfRange,
            _ => Code::Unknown,
        };
        Status::new(code, err.to_string())
    }
}

impl fmt::Display for Status {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "status: {:?}, message: {:?}, details: {:?}, metadata: {:?}",
            self.code(),
            self.message(),
            self.details(),
            self.metadata(),
        )
    }
}

impl Error for Status {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        self.source.as_ref().map(|err| (&**err) as _)
    }
}

///
/// Take the `Status` value from `trailers` if it is available, else from `status_code`.
///
pub(crate) fn infer_grpc_status(
    trailers: Option<&HeaderMap>,
    status_code: http::StatusCode,
) -> Result<(), Option<Status>> {
    if let Some(trailers) = trailers {
        if let Some(status) = Status::from_header_map(trailers) {
            if status.code() == Code::Ok {
                return Ok(());
            } else {
                return Err(status.into());
            }
        }
    }
    trace!("trailers missing grpc-status");
    let code = match status_code {
        // Borrowed from https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md
        http::StatusCode::BAD_REQUEST => Code::Internal,
        http::StatusCode::UNAUTHORIZED => Code::Unauthenticated,
        http::StatusCode::FORBIDDEN => Code::PermissionDenied,
        http::StatusCode::NOT_FOUND => Code::Unimplemented,
        http::StatusCode::TOO_MANY_REQUESTS
        | http::StatusCode::BAD_GATEWAY
        | http::StatusCode::SERVICE_UNAVAILABLE
        | http::StatusCode::GATEWAY_TIMEOUT => Code::Unavailable,
        // We got a 200 but no trailers, we can infer that this request is finished.
        //
        // This can happen when a streaming response sends two Status but
        // gRPC requires that we end the stream after the first status.
        //
        // https://github.com/hyperium/tonic/issues/681
        http::StatusCode::OK => return Err(None),
        _ => Code::Unknown,
    };

    let msg = format!(
        "grpc-status header missing, mapped from HTTP status code {}",
        status_code.as_u16(),
    );
    let status = Status::new(code, msg);
    Err(status.into())
}

// ===== impl Code =====

impl Code {
    /// Get the `Code` that represents the integer, if known.
    ///
    /// If not known, returns `Code::Unknown` (surprise!).
    pub fn from_i32(i: i32) -> Code {
        Code::from(i)
    }

    /// Convert the string representation of a `Code` (as stored, for example, in the `grpc-status`
    /// header in a response) into a `Code`. Returns `Code::Unknown` if the code string is not a
    /// valid gRPC status code.
    pub fn from_bytes(bytes: &[u8]) -> Code {
        match bytes.len() {
            1 => match bytes[0] {
                b'0' => Code::Ok,
                b'1' => Code::Cancelled,
                b'2' => Code::Unknown,
                b'3' => Code::InvalidArgument,
                b'4' => Code::DeadlineExceeded,
                b'5' => Code::NotFound,
                b'6' => Code::AlreadyExists,
                b'7' => Code::PermissionDenied,
                b'8' => Code::ResourceExhausted,
                b'9' => Code::FailedPrecondition,
                _ => Code::parse_err(),
            },
            2 => match (bytes[0], bytes[1]) {
                (b'1', b'0') => Code::Aborted,
                (b'1', b'1') => Code::OutOfRange,
                (b'1', b'2') => Code::Unimplemented,
                (b'1', b'3') => Code::Internal,
                (b'1', b'4') => Code::Unavailable,
                (b'1', b'5') => Code::DataLoss,
                (b'1', b'6') => Code::Unauthenticated,
                _ => Code::parse_err(),
            },
            _ => Code::parse_err(),
        }
    }

    fn to_header_value(self) -> HeaderValue {
        match self {
            Code::Ok => HeaderValue::from_static("0"),
            Code::Cancelled => HeaderValue::from_static("1"),
            Code::Unknown => HeaderValue::from_static("2"),
            Code::InvalidArgument => HeaderValue::from_static("3"),
            Code::DeadlineExceeded => HeaderValue::from_static("4"),
            Code::NotFound => HeaderValue::from_static("5"),
            Code::AlreadyExists => HeaderValue::from_static("6"),
            Code::PermissionDenied => HeaderValue::from_static("7"),
            Code::ResourceExhausted => HeaderValue::from_static("8"),
            Code::FailedPrecondition => HeaderValue::from_static("9"),
            Code::Aborted => HeaderValue::from_static("10"),
            Code::OutOfRange => HeaderValue::from_static("11"),
            Code::Unimplemented => HeaderValue::from_static("12"),
            Code::Internal => HeaderValue::from_static("13"),
            Code::Unavailable => HeaderValue::from_static("14"),
            Code::DataLoss => HeaderValue::from_static("15"),
            Code::Unauthenticated => HeaderValue::from_static("16"),
        }
    }

    fn parse_err() -> Code {
        trace!("error parsing grpc-status");
        Code::Unknown
    }
}

impl From<i32> for Code {
    fn from(i: i32) -> Self {
        match i {
            0 => Code::Ok,
            1 => Code::Cancelled,
            2 => Code::Unknown,
            3 => Code::InvalidArgument,
            4 => Code::DeadlineExceeded,
            5 => Code::NotFound,
            6 => Code::AlreadyExists,
            7 => Code::PermissionDenied,
            8 => Code::ResourceExhausted,
            9 => Code::FailedPrecondition,
            10 => Code::Aborted,
            11 => Code::OutOfRange,
            12 => Code::Unimplemented,
            13 => Code::Internal,
            14 => Code::Unavailable,
            15 => Code::DataLoss,
            16 => Code::Unauthenticated,

            _ => Code::Unknown,
        }
    }
}

impl From<Code> for i32 {
    #[inline]
    fn from(code: Code) -> i32 {
        code as i32
    }
}

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

    #[derive(Debug)]
    struct Nested(Error);

    impl fmt::Display for Nested {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            write!(f, "nested error: {}", self.0)
        }
    }

    impl std::error::Error for Nested {
        fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
            Some(&*self.0)
        }
    }

    #[test]
    fn from_error_status() {
        let orig = Status::new(Code::OutOfRange, "weeaboo");
        let found = Status::from_error(Box::new(orig));

        assert_eq!(found.code(), Code::OutOfRange);
        assert_eq!(found.message(), "weeaboo");
    }

    #[test]
    fn from_error_unknown() {
        let orig: Error = "peek-a-boo".into();
        let found = Status::from_error(orig);

        assert_eq!(found.code(), Code::Unknown);
        assert_eq!(found.message(), "peek-a-boo".to_string());
    }

    #[test]
    fn from_error_nested() {
        let orig = Nested(Box::new(Status::new(Code::OutOfRange, "weeaboo")));
        let found = Status::from_error(Box::new(orig));

        assert_eq!(found.code(), Code::OutOfRange);
        assert_eq!(found.message(), "weeaboo");
    }

    #[test]
    #[cfg(feature = "transport")]
    fn from_error_h2() {
        use std::error::Error as _;

        let orig = h2::Error::from(h2::Reason::CANCEL);
        let found = Status::from_error(Box::new(orig));

        assert_eq!(found.code(), Code::Cancelled);

        let source = found
            .source()
            .and_then(|err| err.downcast_ref::<h2::Error>())
            .unwrap();
        assert_eq!(source.reason(), Some(h2::Reason::CANCEL));
    }

    #[test]
    #[cfg(feature = "transport")]
    fn to_h2_error() {
        let orig = Status::new(Code::Cancelled, "stop eet!");
        let err = orig.to_h2_error();

        assert_eq!(err.reason(), Some(h2::Reason::CANCEL));
    }

    #[test]
    fn code_from_i32() {
        // This for loop should catch if we ever add a new variant and don't
        // update From<i32>.
        for i in 0..(Code::Unauthenticated as i32) {
            let code = Code::from(i);
            assert_eq!(
                i, code as i32,
                "Code::from({}) returned {:?} which is {}",
                i, code, code as i32,
            );
        }

        assert_eq!(Code::from(-1), Code::Unknown);
    }

    #[test]
    fn constructors() {
        assert_eq!(Status::ok("").code(), Code::Ok);
        assert_eq!(Status::cancelled("").code(), Code::Cancelled);
        assert_eq!(Status::unknown("").code(), Code::Unknown);
        assert_eq!(Status::invalid_argument("").code(), Code::InvalidArgument);
        assert_eq!(Status::deadline_exceeded("").code(), Code::DeadlineExceeded);
        assert_eq!(Status::not_found("").code(), Code::NotFound);
        assert_eq!(Status::already_exists("").code(), Code::AlreadyExists);
        assert_eq!(Status::permission_denied("").code(), Code::PermissionDenied);
        assert_eq!(
            Status::resource_exhausted("").code(),
            Code::ResourceExhausted
        );
        assert_eq!(
            Status::failed_precondition("").code(),
            Code::FailedPrecondition
        );
        assert_eq!(Status::aborted("").code(), Code::Aborted);
        assert_eq!(Status::out_of_range("").code(), Code::OutOfRange);
        assert_eq!(Status::unimplemented("").code(), Code::Unimplemented);
        assert_eq!(Status::internal("").code(), Code::Internal);
        assert_eq!(Status::unavailable("").code(), Code::Unavailable);
        assert_eq!(Status::data_loss("").code(), Code::DataLoss);
        assert_eq!(Status::unauthenticated("").code(), Code::Unauthenticated);
    }

    #[test]
    fn details() {
        const DETAILS: &[u8] = &[0, 2, 3];

        let status = Status::with_details(Code::Unavailable, "some message", DETAILS.into());

        assert_eq!(status.details(), DETAILS);

        let header_map = status.to_header_map().unwrap();

        let b64_details = base64::encode_config(DETAILS, base64::STANDARD_NO_PAD);

        assert_eq!(header_map[super::GRPC_STATUS_DETAILS_HEADER], b64_details);

        let status = Status::from_header_map(&header_map).unwrap();

        assert_eq!(status.details(), DETAILS);
    }
}
