use eth2::types::{ErrorMessage, Failure, IndexedErrorMessage};
use std::convert::Infallible;
use std::error::Error;
use std::fmt;
use std::fmt::Debug;
use warp::{Reply, http::StatusCode, reject::Reject, reply::Response};

#[derive(Debug)]
pub struct ServerSentEventError(pub String);

impl Error for ServerSentEventError {}

impl fmt::Display for ServerSentEventError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{:?}", self)
    }
}

pub fn server_sent_event_error(s: String) -> ServerSentEventError {
    ServerSentEventError(s)
}

#[derive(Debug)]
pub struct BeaconStateError(pub types::BeaconStateError);

impl Reject for BeaconStateError {}

pub fn beacon_state_error(e: types::BeaconStateError) -> warp::reject::Rejection {
    warp::reject::custom(BeaconStateError(e))
}

#[derive(Debug)]
pub struct ArithError(pub safe_arith::ArithError);

impl Reject for ArithError {}

pub fn arith_error(e: safe_arith::ArithError) -> warp::reject::Rejection {
    warp::reject::custom(ArithError(e))
}

#[derive(Debug)]
pub struct UnhandledError(pub Box<dyn Debug + Send + Sync + 'static>);

impl Reject for UnhandledError {}

pub fn unhandled_error<D: Debug + Send + Sync + 'static>(e: D) -> warp::reject::Rejection {
    warp::reject::custom(UnhandledError(Box::new(e)))
}

#[derive(Debug)]
pub struct CustomNotFound(pub String);

impl Reject for CustomNotFound {}

pub fn custom_not_found(msg: String) -> warp::reject::Rejection {
    warp::reject::custom(CustomNotFound(msg))
}

#[derive(Debug)]
pub struct CustomBadRequest(pub String);

impl Reject for CustomBadRequest {}

pub fn custom_bad_request(msg: String) -> warp::reject::Rejection {
    warp::reject::custom(CustomBadRequest(msg))
}

#[derive(Debug)]
pub struct CustomDeserializeError(pub String);

impl Reject for CustomDeserializeError {}

pub fn custom_deserialize_error(msg: String) -> warp::reject::Rejection {
    warp::reject::custom(CustomDeserializeError(msg))
}

#[derive(Debug)]
pub struct CustomServerError(pub String);

impl Reject for CustomServerError {}

pub fn custom_server_error(msg: String) -> warp::reject::Rejection {
    warp::reject::custom(CustomServerError(msg))
}

#[derive(Debug)]
pub struct BroadcastWithoutImport(pub String);

impl Reject for BroadcastWithoutImport {}

pub fn broadcast_without_import(msg: String) -> warp::reject::Rejection {
    warp::reject::custom(BroadcastWithoutImport(msg))
}

#[derive(Debug)]
pub struct ObjectInvalid(pub String);

impl Reject for ObjectInvalid {}

pub fn object_invalid(msg: String) -> warp::reject::Rejection {
    warp::reject::custom(ObjectInvalid(msg))
}

#[derive(Debug)]
pub struct NotSynced(pub String);

impl Reject for NotSynced {}

pub fn not_synced(msg: String) -> warp::reject::Rejection {
    warp::reject::custom(NotSynced(msg))
}

/// A 404 Not Found response for when no block has been received for the
/// requested slot.
#[derive(Debug)]
pub struct BlockNotFound(pub String);

impl Reject for BlockNotFound {}

pub fn block_not_found(msg: String) -> warp::reject::Rejection {
    warp::reject::custom(BlockNotFound(msg))
}

#[derive(Debug)]
pub struct InvalidAuthorization(pub String);

impl Reject for InvalidAuthorization {}

pub fn invalid_auth(msg: String) -> warp::reject::Rejection {
    warp::reject::custom(InvalidAuthorization(msg))
}

#[derive(Debug)]
pub struct UnsupportedMediaType(pub String);

impl Reject for UnsupportedMediaType {}

pub fn unsupported_media_type(msg: String) -> warp::reject::Rejection {
    warp::reject::custom(UnsupportedMediaType(msg))
}

#[derive(Debug)]
pub struct IndexedBadRequestErrors {
    pub message: String,
    pub failures: Vec<Failure>,
}

impl Reject for IndexedBadRequestErrors {}

pub fn indexed_bad_request(message: String, failures: Vec<Failure>) -> warp::reject::Rejection {
    warp::reject::custom(IndexedBadRequestErrors { message, failures })
}

/// This function receives a `Rejection` and tries to return a custom
/// value, otherwise simply passes the rejection along.
pub async fn handle_rejection(err: warp::Rejection) -> Result<impl warp::Reply, Infallible> {
    let code;
    let message;

    if let Some(e) = err.find::<crate::reject::IndexedBadRequestErrors>() {
        message = format!("BAD_REQUEST: {}", e.message);
        code = StatusCode::BAD_REQUEST;

        let json = warp::reply::json(&IndexedErrorMessage {
            code: code.as_u16(),
            message,
            failures: e.failures.clone(),
        });

        return Ok(warp::reply::with_status(json, code));
    }

    if err.is_not_found() {
        code = StatusCode::NOT_FOUND;
        message = "NOT_FOUND".to_string();
    } else if err.find::<crate::reject::UnsupportedMediaType>().is_some() {
        code = StatusCode::UNSUPPORTED_MEDIA_TYPE;
        message = "UNSUPPORTED_MEDIA_TYPE".to_string();
    } else if let Some(e) = err.find::<crate::reject::CustomDeserializeError>() {
        message = format!("BAD_REQUEST: body deserialize error: {}", e.0);
        code = StatusCode::BAD_REQUEST;
    } else if let Some(e) = err.find::<warp::filters::body::BodyDeserializeError>() {
        message = format!("BAD_REQUEST: body deserialize error: {}", e);
        code = StatusCode::BAD_REQUEST;
    } else if let Some(e) = err.find::<warp::reject::InvalidQuery>() {
        code = StatusCode::BAD_REQUEST;
        message = format!("BAD_REQUEST: invalid query: {}", e);
    } else if let Some(e) = err.find::<crate::reject::UnhandledError>() {
        code = StatusCode::INTERNAL_SERVER_ERROR;
        message = format!("UNHANDLED_ERROR: {:?}", e.0);
    } else if let Some(e) = err.find::<crate::reject::CustomNotFound>() {
        code = StatusCode::NOT_FOUND;
        message = format!("NOT_FOUND: {}", e.0);
    } else if let Some(e) = err.find::<crate::reject::CustomBadRequest>() {
        code = StatusCode::BAD_REQUEST;
        message = format!("BAD_REQUEST: {}", e.0);
    } else if let Some(e) = err.find::<crate::reject::CustomServerError>() {
        code = StatusCode::INTERNAL_SERVER_ERROR;
        message = format!("INTERNAL_SERVER_ERROR: {}", e.0);
    } else if let Some(e) = err.find::<crate::reject::BroadcastWithoutImport>() {
        code = StatusCode::ACCEPTED;
        message = format!(
            "ACCEPTED: the object was broadcast to the network without being \
            fully imported to the local database: {}",
            e.0
        );
    } else if let Some(e) = err.find::<crate::reject::ObjectInvalid>() {
        code = StatusCode::BAD_REQUEST;
        message = format!("BAD_REQUEST: Invalid object: {}", e.0);
    } else if let Some(e) = err.find::<crate::reject::NotSynced>() {
        code = StatusCode::SERVICE_UNAVAILABLE;
        message = format!("SERVICE_UNAVAILABLE: beacon node is syncing: {}", e.0);
    } else if let Some(e) = err.find::<crate::reject::BlockNotFound>() {
        code = StatusCode::NOT_FOUND;
        message = format!("NOT_FOUND: {}", e.0);
    } else if let Some(e) = err.find::<crate::reject::InvalidAuthorization>() {
        code = StatusCode::FORBIDDEN;
        message = format!("FORBIDDEN: Invalid auth token: {}", e.0);
    } else if let Some(e) = err.find::<warp::reject::MissingHeader>() {
        if e.name().eq("Authorization") {
            code = StatusCode::UNAUTHORIZED;
            message = "UNAUTHORIZED: missing Authorization header".to_string();
        } else {
            code = StatusCode::BAD_REQUEST;
            message = format!("BAD_REQUEST: missing {} header", e.name());
        }
    } else if let Some(e) = err.find::<warp::reject::InvalidHeader>() {
        code = StatusCode::BAD_REQUEST;
        message = format!("BAD_REQUEST: invalid {} header", e.name());
    } else if err.find::<warp::reject::MethodNotAllowed>().is_some() {
        code = StatusCode::METHOD_NOT_ALLOWED;
        message = "METHOD_NOT_ALLOWED".to_string();
    } else {
        code = StatusCode::INTERNAL_SERVER_ERROR;
        message = "UNHANDLED_REJECTION".to_string();
    }

    let json = warp::reply::json(&ErrorMessage {
        code: code.as_u16(),
        message,
        stacktraces: vec![],
    });

    Ok(warp::reply::with_status(json, code))
}

/// Convert a warp `Rejection` into a `Response`.
///
/// This function should *always* be used to convert rejections into responses. This prevents warp
/// from trying to backtrack in strange ways. See: https://github.com/sigp/lighthouse/issues/3404
pub async fn convert_rejection<T: Reply>(res: Result<T, warp::Rejection>) -> Response {
    match res {
        Ok(response) => response.into_response(),
        Err(e) => {
            let Ok(reply) = handle_rejection(e).await;
            reply.into_response()
        }
    }
}
