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

#[derive(Debug)]
pub enum AppError {
    BadRequest(String),
    Unauthorized(String),
    Forbidden(String),
    /// Uploads are temporarily locked (event closed / gallery released). Distinct from
    /// `Forbidden` so the client can tell this REVERSIBLE 403 apart from a permanent one
    /// (banned user, quota): the queued blob is kept and retried if the host reopens,
    /// instead of being purged like a genuinely-terminal rejection.
    UploadsLocked(String),
    NotFound(String),
    Conflict(String),
    /// Second field: optional retry-after seconds to include in the response.
    TooManyRequests(String, Option<u64>),
    /// Per-user storage quota exhausted. Distinct from `TooManyRequests` (rate limit) so
    /// the client can treat it as *terminal* (413, no retry) instead of backing off and
    /// retrying a permanently-failing upload forever.
    QuotaExceeded(String),
    Internal(anyhow::Error),
}

impl AppError {
    fn status_and_code(&self) -> (StatusCode, &str) {
        match self {
            Self::BadRequest(_) => (StatusCode::BAD_REQUEST, "bad_request"),
            Self::Unauthorized(_) => (StatusCode::UNAUTHORIZED, "unauthorized"),
            Self::Forbidden(_) => (StatusCode::FORBIDDEN, "forbidden"),
            Self::UploadsLocked(_) => (StatusCode::FORBIDDEN, "uploads_locked"),
            Self::NotFound(_) => (StatusCode::NOT_FOUND, "not_found"),
            Self::Conflict(_) => (StatusCode::CONFLICT, "conflict"),
            Self::TooManyRequests(..) => (StatusCode::TOO_MANY_REQUESTS, "too_many_requests"),
            Self::QuotaExceeded(_) => (StatusCode::PAYLOAD_TOO_LARGE, "quota_exceeded"),
            Self::Internal(_) => (StatusCode::INTERNAL_SERVER_ERROR, "internal_error"),
        }
    }

    fn message(&self) -> String {
        match self {
            Self::BadRequest(msg)
            | Self::Unauthorized(msg)
            | Self::Forbidden(msg)
            | Self::UploadsLocked(msg)
            | Self::NotFound(msg)
            | Self::Conflict(msg) => msg.clone(),
            Self::TooManyRequests(msg, _) => msg.clone(),
            Self::QuotaExceeded(msg) => msg.clone(),
            Self::Internal(err) => {
                tracing::error!("internal error: {err:#}");
                "Ein interner Fehler ist aufgetreten.".to_string()
            }
        }
    }
}

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        let (status, code) = self.status_and_code();
        let retry_after_secs = if let Self::TooManyRequests(_, Some(secs)) = &self {
            Some(*secs)
        } else {
            None
        };
        let message = self.message();

        let mut body = serde_json::json!({
            "error": code,
            "message": message,
            "status": status.as_u16(),
        });
        if let Some(secs) = retry_after_secs {
            body["retry_after_secs"] = secs.into();
        }

        let mut resp = (status, axum::Json(body)).into_response();
        if let Some(secs) = retry_after_secs
            && let Ok(val) = axum::http::HeaderValue::from_str(&secs.to_string())
        {
            resp.headers_mut()
                .insert(axum::http::header::RETRY_AFTER, val);
        }
        resp
    }
}

impl From<anyhow::Error> for AppError {
    fn from(err: anyhow::Error) -> Self {
        Self::Internal(err)
    }
}

impl From<sqlx::Error> for AppError {
    fn from(err: sqlx::Error) -> Self {
        Self::Internal(err.into())
    }
}
