use actix_web::{HttpResponse, ResponseError};
use serde::Serialize;
use service::error::AppError;
use utoipa::openapi::schema::{KnownFormat, ObjectBuilder, SchemaFormat, Type};
use utoipa::openapi::{RefOr, Schema};
use utoipa::{PartialSchema, ToSchema};

#[derive(Debug, Serialize, ToSchema)]
pub struct ApiResponse<T> {
    pub code: i32,
    pub message: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub data: Option<T>,
}

impl<T: Serialize> ApiResponse<T> {
    pub fn ok(data: T) -> Self {
        Self {
            code: 0,
            message: "ok".to_string(),
            data: Some(data),
        }
    }

    pub fn to_response(self) -> HttpResponse {
        HttpResponse::Ok().json(self)
    }
}

pub fn api_success() -> HttpResponse {
    HttpResponse::Ok().json(ApiResponse {
        code: 0,
        message: "ok".to_string(),
        data: None::<()>,
    })
}

#[derive(Debug, Serialize, ToSchema)]
pub struct ApiErrorResponse {
    pub code: i32,
    pub error: String,
    pub message: String,
}

#[derive(Debug)]
pub struct ApiError(pub AppError);

impl std::fmt::Display for ApiError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        self.0.user_message().fmt(f)
    }
}

impl std::error::Error for ApiError {}

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

impl From<room::RoomError> for ApiError {
    fn from(e: room::RoomError) -> Self {
        ApiError(e.into())
    }
}

impl ResponseError for ApiError {
    fn error_response(&self) -> HttpResponse {
        let err = &self.0;
        let status = actix_web::http::StatusCode::from_u16(err.http_status_code())
            .unwrap_or(actix_web::http::StatusCode::INTERNAL_SERVER_ERROR);
        let resp = ApiErrorResponse {
            code: err.code(),
            error: err.slug().to_string(),
            message: err.user_message(),
        };
        HttpResponse::build(status).json(resp)
    }
}

impl PartialSchema for ApiError {
    fn schema() -> RefOr<Schema> {
        RefOr::T(Schema::Object(
            ObjectBuilder::new()
                .property(
                    "code",
                    ObjectBuilder::new()
                        .schema_type(Type::Integer)
                        .format(Some(SchemaFormat::KnownFormat(KnownFormat::Int32)))
                        .description(Some("Error numeric code")),
                )
                .property(
                    "error",
                    ObjectBuilder::new()
                        .schema_type(Type::String)
                        .description(Some("Error slug identifier")),
                )
                .property(
                    "message",
                    ObjectBuilder::new()
                        .schema_type(Type::String)
                        .description(Some("Human-readable error message")),
                )
                .required("code")
                .required("error")
                .required("message")
                .into(),
        ))
    }
}

impl ToSchema for ApiError {}
