//! Uniform HTTP error mapping for the API layer (v0.5.4).
//!
//! Before this, ~40 handlers in `app.rs` hand-wrote
//! `match … { Err(e) => (StatusCode::…, format!("{e}")).into_response() }`,
//! and the `openpxe_core::Error` → status mapping drifted between them
//! (e.g. `Invalid` → 400 in most places, 404 in one). [`AppError`] wraps
//! `openpxe_core::Error` so a handler can return `Result<T, AppError>` and
//! `?` its way out, getting one consistent status + body. The body stays
//! plain-text (matching the previous `(StatusCode, String)` responses) so
//! existing clients and tests see no shape change; 5xx detail is logged
//! and returned verbatim exactly as before.
//!
//! Handlers with *intentional* domain-specific statuses (e.g. a duplicate
//! share → 409, a still-open chunked upload → 409) keep their explicit
//! returns — `AppError` is for the common case, not a straitjacket.

use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use openpxe_core::Error as CoreError;

/// Newtype over [`openpxe_core::Error`] with a uniform [`IntoResponse`].
#[derive(Debug)]
pub struct AppError(pub CoreError);

impl From<CoreError> for AppError {
    fn from(e: CoreError) -> Self {
        AppError(e)
    }
}

impl From<std::io::Error> for AppError {
    fn from(e: std::io::Error) -> Self {
        AppError(CoreError::Io(e))
    }
}

impl AppError {
    /// The HTTP status this error maps to. Public so handlers (and tests)
    /// can reason about the mapping in one place.
    #[must_use]
    pub fn status(&self) -> StatusCode {
        match self.0 {
            CoreError::NotFound(_) => StatusCode::NOT_FOUND,
            CoreError::Invalid(_) => StatusCode::BAD_REQUEST,
            CoreError::Config(_) | CoreError::Io(_) | CoreError::Other(_) => {
                StatusCode::INTERNAL_SERVER_ERROR
            }
        }
    }
}

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        let status = self.status();
        // Match the prior hand-written responses: the 4xx arms returned the
        // bare inner message (not the `Display` prefix), so a UI showing
        // `await r.text()` reads "metadata too long", not "invalid input:
        // metadata too long". 5xx keeps the full `Display` string.
        let body = match &self.0 {
            CoreError::Invalid(m) | CoreError::NotFound(m) => m.clone(),
            other => other.to_string(),
        };
        if status.is_server_error() {
            // Log the full detail server-side; the body still carries it
            // (unchanged from the prior `format!("{e}")` behaviour), but the
            // log line is what an operator greps for.
            tracing::error!(target: "openpxe::http", error = %self.0, "request failed");
        }
        (status, body).into_response()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn status_mapping_is_consistent() {
        assert_eq!(
            AppError(CoreError::NotFound("x".into())).status(),
            StatusCode::NOT_FOUND
        );
        assert_eq!(
            AppError(CoreError::Invalid("x".into())).status(),
            StatusCode::BAD_REQUEST
        );
        assert_eq!(
            AppError(CoreError::Config("x".into())).status(),
            StatusCode::INTERNAL_SERVER_ERROR
        );
    }
}
