use std::convert::Infallible;
use std::error::Error as StdError;
use std::fmt::{self, Display};
use std::io;
use std::result;

/// Errors than can occur while parsing the response from the server.
#[derive(Debug)]
pub enum InvalidResponseKind {
    /// Invalid or missing Location header in redirection
    LocationHeader,
    /// Invalid redirection URL
    RedirectionUrl,
    /// Status line
    StatusLine,
    /// Status code
    StatusCode,
    /// Error parsing header
    Header,
    /// Error decoding chunk size
    ChunkSize,
    /// Error decoding chunk
    Chunk,
    /// Invalid Content-Length header
    ContentLength,
}

impl Display for InvalidResponseKind {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        use InvalidResponseKind::*;

        match self {
            LocationHeader => write!(f, "missing or invalid location header"),
            RedirectionUrl => write!(f, "invalid redirection url"),
            StatusLine => write!(f, "invalid status line"),
            StatusCode => write!(f, "invalid status code"),
            Header => write!(f, "invalid header"),
            ChunkSize => write!(f, "invalid chunk size"),
            Chunk => write!(f, "invalid chunk"),
            ContentLength => write!(f, "invalid content length"),
        }
    }
}

/// Common errors that can occur during HTTP requests.
#[derive(Debug)]
pub enum ErrorKind {
    /// CONNECT is not supported.
    ConnectNotSupported,
    /// Could not connect to proxy with CONNECT method.
    ConnectError {
        /// Status code from the proxy.
        status_code: http::StatusCode,
        /// Up to 10 KiB of body data from the proxy which might help diagnose the error.
        body: Vec<u8>,
    },
    /// Error generated by the `http` crate.
    Http(http::Error),
    /// IO Error
    Io(io::Error),
    /// Invalid base URL given to the Request.
    InvalidBaseUrl,
    /// An URL with an invalid host was found while processing the request.
    InvalidUrlHost,
    /// The URL scheme is unknown and the port is missing.
    InvalidUrlPort,
    /// Server sent an invalid response.
    InvalidResponse(InvalidResponseKind),
    /// Too many redirections
    TooManyRedirections,
    /// Status code indicates failure
    StatusCode(http::StatusCode),
    /// JSON decoding/encoding error.
    #[cfg(feature = "json")]
    Json(serde_json::Error),
    /// Form-URL encoding error.
    #[cfg(feature = "form")]
    UrlEncoded(serde_urlencoded::ser::Error),
    /// TLS error encountered while connecting to an https server.
    #[cfg(feature = "tls-native")]
    Tls(native_tls::Error),
    /// TLS error encountered while connecting to an https server.
    #[cfg(all(feature = "__rustls", not(feature = "tls-native")))]
    Tls(rustls::Error),
    /// Invalid DNS name used for TLS certificate verification
    #[cfg(feature = "__rustls")]
    InvalidDNSName(String),
    /// Invalid mime type in a Multipart form
    InvalidMimeType(String),
    /// TLS was not enabled by features.
    TlsDisabled,
    /// Empty cert store
    #[cfg(all(feature = "__rustls", not(feature = "tls-native")))]
    ServerCertVerifier(rustls::client::VerifierBuilderError),
}

/// A type that contains all the errors that can possibly occur while accessing an HTTP server.
#[derive(Debug)]
pub struct Error(pub(crate) Box<ErrorKind>);

impl Error {
    /// Get a reference to the `ErrorKind` inside.
    pub fn kind(&self) -> &ErrorKind {
        &self.0
    }

    /// Comsume this `Error` and get the `ErrorKind` inside.
    pub fn into_kind(self) -> ErrorKind {
        *self.0
    }
}

impl Display for Error {
    fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result {
        use ErrorKind::*;

        match *self.0 {
            ConnectNotSupported => write!(w, "CONNECT is not supported"),
            ConnectError { status_code, .. } => write!(w, "Proxy CONNECT error: {status_code}"),
            Http(ref e) => write!(w, "Http Error: {e}"),
            Io(ref e) => write!(w, "Io Error: {e}"),
            InvalidBaseUrl => write!(w, "Invalid base URL"),
            InvalidUrlHost => write!(w, "URL is missing a host"),
            InvalidUrlPort => write!(w, "URL is missing a port"),
            InvalidResponse(ref k) => write!(w, "InvalidResponse: {k}"),
            TooManyRedirections => write!(w, "Too many redirections"),
            StatusCode(ref sc) => write!(w, "Status code {sc} indicates failure"),
            #[cfg(feature = "json")]
            Json(ref e) => write!(w, "Json Error: {e}"),
            #[cfg(feature = "form")]
            UrlEncoded(ref e) => write!(w, "URL Encoding Error: {e}"),
            #[cfg(any(feature = "tls-native", feature = "__rustls"))]
            Tls(ref e) => write!(w, "Tls Error: {e}"),
            #[cfg(feature = "__rustls")]
            InvalidDNSName(ref e) => write!(w, "Invalid DNS name: {e}"),
            InvalidMimeType(ref e) => write!(w, "Invalid mime type: {e}"),
            TlsDisabled => write!(w, "TLS is disabled, activate one of the tls- features"),
            #[cfg(all(feature = "__rustls", not(feature = "tls-native")))]
            ServerCertVerifier(ref e) => write!(w, "Invalid certificate: {e}"),
        }
    }
}

impl StdError for Error {
    fn cause(&self) -> Option<&dyn StdError> {
        use ErrorKind::*;

        match *self.0 {
            Io(ref e) => Some(e),
            Http(ref e) => Some(e),
            #[cfg(feature = "json")]
            Json(ref e) => Some(e),
            #[cfg(any(feature = "tls-native", feature = "__rustls"))]
            Tls(ref e) => Some(e),
            _ => None,
        }
    }
}

impl From<Infallible> for Error {
    fn from(_err: Infallible) -> Error {
        unreachable!()
    }
}

impl From<io::Error> for Error {
    fn from(err: io::Error) -> Error {
        Error(Box::new(ErrorKind::Io(err)))
    }
}

impl From<http::Error> for Error {
    fn from(err: http::Error) -> Error {
        Error(Box::new(ErrorKind::Http(err)))
    }
}

impl From<http::header::InvalidHeaderValue> for Error {
    fn from(err: http::header::InvalidHeaderValue) -> Error {
        Error(Box::new(ErrorKind::Http(http::Error::from(err))))
    }
}

#[cfg(feature = "tls-native")]
impl From<native_tls::Error> for Error {
    fn from(err: native_tls::Error) -> Error {
        Error(Box::new(ErrorKind::Tls(err)))
    }
}

#[cfg(all(feature = "__rustls", not(feature = "tls-native")))]
impl From<rustls::Error> for Error {
    fn from(err: rustls::Error) -> Error {
        Error(Box::new(ErrorKind::Tls(err)))
    }
}

#[cfg(feature = "json")]
impl From<serde_json::Error> for Error {
    fn from(err: serde_json::Error) -> Error {
        Error(Box::new(ErrorKind::Json(err)))
    }
}

#[cfg(feature = "form")]
impl From<serde_urlencoded::ser::Error> for Error {
    fn from(err: serde_urlencoded::ser::Error) -> Error {
        Error(Box::new(ErrorKind::UrlEncoded(err)))
    }
}

impl From<ErrorKind> for Error {
    fn from(err: ErrorKind) -> Error {
        Error(Box::new(err))
    }
}

impl From<InvalidResponseKind> for Error {
    fn from(kind: InvalidResponseKind) -> Error {
        ErrorKind::InvalidResponse(kind).into()
    }
}

#[cfg(all(feature = "__rustls", not(feature = "tls-native")))]
impl From<rustls::client::VerifierBuilderError> for Error {
    fn from(err: rustls::client::VerifierBuilderError) -> Error {
        Error(Box::new(ErrorKind::ServerCertVerifier(err)))
    }
}

impl From<Error> for io::Error {
    fn from(err: Error) -> io::Error {
        io::Error::new(io::ErrorKind::Other, err)
    }
}

impl From<InvalidResponseKind> for io::Error {
    fn from(kind: InvalidResponseKind) -> io::Error {
        io::Error::new(io::ErrorKind::Other, Error(Box::new(ErrorKind::InvalidResponse(kind))))
    }
}

/// Wrapper for the `Result` type with an `Error`.
pub type Result<T = ()> = result::Result<T, Error>;
