use std::fmt::{self, Display};

use axum::http::StatusCode;
use axum::response::{IntoResponse, Redirect, Response};
use validator::ValidationErrors;

#[derive(Debug)]
pub struct AuthRedirectInfo {
    base_path: String,
}

// Use anyhow, define error and enable '?'
// For a simplified example of using anyhow in axum check /examples/anyhow-error-response
#[derive(Debug)]
pub enum AppError {
    InternalServerError(anyhow::Error),
    ForbiddenError(String),
    NotFoundError(String),
    BadRequestError(String),
    TooManyRequestsError(String),
    AuthRedirect(AuthRedirectInfo),
}

impl AppError {
    pub fn auth_redirect_from_base_path(base_path: String) -> Self {
        Self::AuthRedirect(AuthRedirectInfo { base_path })
    }

    pub fn from_validation_errors(errs: ValidationErrors) -> Self {
        Self::BadRequestError(
            serde_json::to_string(&errs).unwrap_or("validation error".to_string()),
        )
    }
}

// Tell axum how to convert `AppError` into a response.
impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        match self {
            Self::AuthRedirect(AuthRedirectInfo { base_path }) => {
                tracing::debug!("Handling AuthRedirect");
                Redirect::to(&format!("{}/auth/login", base_path)).into_response()
            }
            Self::InternalServerError(err) => {
                tracing::error!("Application error: {:?}", err);
                (StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong").into_response()
            }
            Self::ForbiddenError(client_message) => {
                tracing::info!("Forbidden: {}", client_message);
                (StatusCode::FORBIDDEN, client_message).into_response()
            }
            Self::NotFoundError(client_message) => {
                tracing::info!("Not found: {}", client_message);
                (StatusCode::NOT_FOUND, client_message).into_response()
            }
            Self::TooManyRequestsError(client_message) => {
                // Debug level so that if this is from a runaway loop, it won't
                // overwhelm server logs
                tracing::debug!("Too many requests: {}", client_message);
                (StatusCode::TOO_MANY_REQUESTS, client_message).into_response()
            }
            Self::BadRequestError(client_message) => {
                tracing::info!("Bad user input: {}", client_message);
                (StatusCode::BAD_REQUEST, client_message).into_response()
            }
        }
    }
}

// This enables using `?` on functions that return `Result<_, anyhow::Error>` to turn them into
// `Result<_, AppError>`. That way you don't need to do that manually.
impl<E> From<E> for AppError
where
    E: Into<anyhow::Error>,
{
    fn from(err: E) -> Self {
        Self::InternalServerError(Into::<anyhow::Error>::into(err))
    }
}

impl Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            AppError::AuthRedirect(info) => write!(f, "AuthRedirect: {:?}", info),
            AppError::InternalServerError(inner) => inner.fmt(f),
            AppError::ForbiddenError(client_message) => {
                write!(f, "ForbiddenError: {}", client_message)
            }
            AppError::NotFoundError(client_message) => {
                write!(f, "NotFoundError: {}", client_message)
            }
            AppError::BadRequestError(client_message) => {
                write!(f, "BadRequestError: {}", client_message)
            }
            AppError::TooManyRequestsError(client_message) => {
                write!(f, "TooManyRequestsError: {}", client_message)
            }
        }
    }
}
