use crate::context::RequestContext;
use axum::http::StatusCode;
use axum::http::{HeaderName, HeaderValue};
use axum::response::{IntoResponse, Response};
use axum::Json;
use serde::Serialize;

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ErrorBody {
    pub ok: bool,
    pub code: &'static str,
    pub message: String,
    pub request_id: Option<String>,
    pub trace_id: Option<String>,
}

#[derive(Debug, Clone)]
pub struct WebError {
    status: StatusCode,
    code: &'static str,
    message: String,
    request_context: Option<RequestContext>,
    headers: Vec<(&'static str, String)>,
}

impl WebError {
    pub fn new(status: StatusCode, code: &'static str, message: impl Into<String>) -> Self {
        Self {
            status,
            code,
            message: message.into(),
            request_context: None,
            headers: Vec::new(),
        }
    }

    pub fn bad_request(message: impl Into<String>) -> Self {
        Self::new(StatusCode::BAD_REQUEST, "bad_request", message)
    }

    pub fn bad_request_code(code: &'static str, message: impl Into<String>) -> Self {
        Self::new(StatusCode::BAD_REQUEST, code, message)
    }

    pub fn internal(message: impl Into<String>) -> Self {
        Self::new(StatusCode::INTERNAL_SERVER_ERROR, "internal_error", message)
    }

    pub fn bad_gateway_code(code: &'static str, message: impl Into<String>) -> Self {
        Self::new(StatusCode::BAD_GATEWAY, code, message)
    }

    pub fn service_unavailable_code(code: &'static str, message: impl Into<String>) -> Self {
        Self::new(StatusCode::SERVICE_UNAVAILABLE, code, message)
    }

    pub fn gateway_timeout_code(code: &'static str, message: impl Into<String>) -> Self {
        Self::new(StatusCode::GATEWAY_TIMEOUT, code, message)
    }

    pub fn with_context(mut self, request_context: &RequestContext) -> Self {
        self.request_context = Some(request_context.clone());
        self
    }

    pub fn with_header(mut self, name: &'static str, value: impl Into<String>) -> Self {
        self.headers.push((name, value.into()));
        self
    }
}

impl IntoResponse for WebError {
    fn into_response(self) -> Response {
        let body = ErrorBody {
            ok: false,
            code: self.code,
            message: self.message,
            request_id: self
                .request_context
                .as_ref()
                .map(|context| context.trace.request_id.clone()),
            trace_id: self
                .request_context
                .as_ref()
                .map(|context| context.trace.trace_id.clone()),
        };
        let mut response = (self.status, Json(body)).into_response();
        if let Ok(name) = HeaderName::from_lowercase(b"x-error-code") {
            if let Ok(value) = HeaderValue::from_str(self.code) {
                response.headers_mut().insert(name, value);
            }
        }
        for (name, value) in self.headers {
            let Ok(header_name) = HeaderName::from_lowercase(name.as_bytes()) else {
                continue;
            };
            let Ok(header_value) = HeaderValue::from_str(&value) else {
                continue;
            };
            response.headers_mut().insert(header_name, header_value);
        }
        response
    }
}
