//! Typed appview error type.
//!
//! Handlers return `Result<XrpcResponse<_>, AppError>`. `AppError` is a small
//! typed enum (not a string bag): each variant maps to a fixed HTTP status and
//! XRPC error code at the [`IntoResponse`] boundary, so the error *code* is a
//! compile-time constant and only the human message is a `String`. Internal
//! details are logged; the response body never carries secrets or raw errors.

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

/// Appview handler error. Maps onto the XRPC error response shape.
#[derive(Debug)]
pub(crate) enum AppError {
    /// 401 `AuthenticationRequired`: strict OAuth extraction failed or an
    /// authenticated PDS operation reported an auth failure.
    Unauthorized(String),
    /// 400 `InvalidRequest`: bad parameters (bad at-uri, unknown algorithm,
    /// unparseable cursor, unresolvable handle).
    InvalidRequest(String),
    /// 404 `RecordNotFound`: the requested record does not exist in the index.
    NotFound,
    /// 500 `InternalServerError`: unexpected failure (database, record decode,
    /// upstream bsky fetch). The message is safe to expose; details are logged.
    Internal(String),
}

pub(crate) type AppResult<T> = Result<T, AppError>;

pub(super) fn invalid_request(msg: impl Into<String>) -> AppError {
    AppError::InvalidRequest(msg.into())
}

pub(super) fn unauthorized(msg: impl Into<String>) -> AppError {
    AppError::Unauthorized(msg.into())
}

pub(super) fn not_found() -> AppError {
    AppError::NotFound
}

pub(super) fn internal(msg: impl Into<String>) -> AppError {
    AppError::Internal(msg.into())
}

/// Adapt a `sqlx::Error` into a safe 500, logging the real error server-side.
pub(super) fn db<T>(result: Result<T, sqlx::Error>) -> AppResult<T> {
    result.map_err(|e| {
        tracing::error!(error = %e, "database error");
        AppError::Internal("database error".to_string())
    })
}

/// Clamp a page limit into [1, 100].
pub(super) fn clamp_limit(limit: Option<i64>) -> i64 {
    limit.unwrap_or(50).clamp(1, 100)
}

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        let (status, code, message) = match self {
            AppError::Unauthorized(m) => {
                (StatusCode::UNAUTHORIZED, "AuthenticationRequired", Some(m))
            }
            AppError::InvalidRequest(m) => (StatusCode::BAD_REQUEST, "InvalidRequest", Some(m)),
            AppError::NotFound => (StatusCode::NOT_FOUND, "RecordNotFound", None),
            AppError::Internal(m) => (
                StatusCode::INTERNAL_SERVER_ERROR,
                "InternalServerError",
                Some(m),
            ),
        };
        GenericXrpcErrorResponse::new(status, code, message).into_response()
    }
}
