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

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

macro_rules! forbidden {
    ($message:literal) => {
        crate::errors::AppError::Forbidden($message.to_owned())
    };

    ($message:literal, $($param:expr),+) => {
        crate::errors::AppError::Forbidden(format!($message, $($param)+))
    };
}

macro_rules! not_found {
    ($message:literal) => {
        crate::errors::AppError::NotFound($message.to_owned())
    };

    ($message:literal, $($param:expr),+) => {
        crate::errors::AppError::NotFound(format!($message, $($param)+))
    };
}

macro_rules! bad_request {
    ($message:literal) => {
        crate::errors::AppError::BadRequest(format!($message))
    };

    ($message:literal, $($param:expr),+) => {
        crate::errors::AppError::BadRequest(format!($message, $($param)+))
    };
}

pub(crate) use bad_request;
pub(crate) use forbidden;
pub(crate) use not_found;

/// Custom error type that maps to appropriate HTTP responses.
#[derive(Debug)]
pub enum AppError {
    InternalServerError(anyhow::Error),
    Forbidden(String),
    NotFound(String),
    BadRequest(String),
    TooManyRequests(String),
}

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        match self {
            Self::InternalServerError(err) => {
                tracing::error!("Application error: {:?}", err);
                (StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong").into_response()
            }
            Self::Forbidden(client_message) => {
                tracing::info!("Forbidden: {}", client_message);
                (StatusCode::FORBIDDEN, client_message).into_response()
            }
            Self::NotFound(client_message) => {
                tracing::info!("Not found: {}", client_message);
                (StatusCode::NOT_FOUND, client_message).into_response()
            }
            Self::TooManyRequests(client_message) => {
                // Debug level so that if this is from a runaway loop, it won't
                // overwhelm production logs.
                tracing::debug!("Too many requests: {}", client_message);
                (StatusCode::TOO_MANY_REQUESTS, client_message).into_response()
            }
            Self::BadRequest(client_message) => {
                tracing::info!("Bad user input: {}", client_message);
                (StatusCode::BAD_REQUEST, client_message).into_response()
            }
        }
    }
}

// Easily convert semi-arbitrary errors to InternalServerError.
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::InternalServerError(inner) => inner.fmt(f),
            AppError::Forbidden(client_message) => {
                write!(f, "ForbiddenError: {client_message}")
            }
            AppError::NotFound(client_message) => {
                write!(f, "NotFoundError: {client_message}")
            }
            AppError::BadRequest(client_message) => {
                write!(f, "BadRequestError: {client_message}")
            }
            AppError::TooManyRequests(client_message) => {
                write!(f, "TooManyRequestsError: {client_message}")
            }
        }
    }
}
