use crate::model::processing::PackError;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use axum::Json;
use magnetar_common::util::FediverseTagParseError;
use magnetar_model::{CalckeyCacheError, CalckeyDbError};
use serde::Serialize;
use serde_json::json;
use std::fmt::{Display, Formatter};
use thiserror::Error;

pub mod auth;
pub mod extractors;
pub mod pagination;

#[derive(Debug, Clone, Serialize)]
#[repr(transparent)]
pub struct ErrorCode(pub String);

// This janky hack allows us to use `.error_code()` on enums with strum::IntoStaticStr
pub trait IntoErrorCode {
    fn error_code<'a, 'b: 'a>(&'a self) -> ErrorCode
    where
        &'a Self: Into<&'b str>;
}

impl<T: ?Sized> IntoErrorCode for T {
    fn error_code<'a, 'b: 'a>(&'a self) -> ErrorCode
    where
        &'a Self: Into<&'b str>,
    {
        ErrorCode(<&Self as Into<&'b str>>::into(self).to_string())
    }
}

impl ErrorCode {
    pub fn join(&self, other: &str) -> Self {
        Self(format!("{}:{}", other, self.0))
    }
}

#[derive(Debug, Error)]
pub struct ApiError {
    pub status: StatusCode,
    pub code: ErrorCode,
    pub message: String,
}

impl Display for ApiError {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "ApiError[status = \"{}\", code = \"{:?}\"]: \"{}\"",
            self.status, self.code, self.message
        )
    }
}

#[derive(Debug)]
pub struct AccessForbidden(pub String);

impl From<&AccessForbidden> for &str {
    fn from(_: &AccessForbidden) -> &'static str {
        "AccessForbidden"
    }
}

impl IntoResponse for ApiError {
    fn into_response(self) -> Response {
        (
            self.status,
            Json(json!({
                "status": self.status.as_u16(),
                "code": self.code,
                "message": self.message,
            })),
        )
            .into_response()
    }
}

impl From<AccessForbidden> for ApiError {
    fn from(err: AccessForbidden) -> Self {
        Self {
            status: StatusCode::FORBIDDEN,
            code: err.error_code(),
            message: if cfg!(debug_assertions) {
                format!("Forbidden: {}", err.0)
            } else {
                "Forbidden".to_string()
            },
        }
    }
}

impl From<FediverseTagParseError> for ApiError {
    fn from(err: FediverseTagParseError) -> Self {
        Self {
            status: StatusCode::BAD_REQUEST,
            code: err.error_code(),
            message: if cfg!(debug_assertions) {
                format!("Fediverse tag parse error: {}", err)
            } else {
                "Fediverse tag parse error".to_string()
            },
        }
    }
}

impl From<CalckeyDbError> for ApiError {
    fn from(err: CalckeyDbError) -> Self {
        Self {
            status: StatusCode::INTERNAL_SERVER_ERROR,
            code: err.error_code(),
            message: if cfg!(debug_assertions) {
                format!("Database error: {}", err)
            } else {
                "Database error".to_string()
            },
        }
    }
}

impl From<CalckeyCacheError> for ApiError {
    fn from(err: CalckeyCacheError) -> Self {
        Self {
            status: StatusCode::INTERNAL_SERVER_ERROR,
            code: err.error_code(),
            message: if cfg!(debug_assertions) {
                format!("Cache error: {}", err)
            } else {
                "Cache error".to_string()
            },
        }
    }
}

impl From<PackError> for ApiError {
    fn from(err: PackError) -> Self {
        Self {
            status: StatusCode::INTERNAL_SERVER_ERROR,
            code: err.error_code(),
            message: if cfg!(debug_assertions) {
                format!("Data transformation error: {}", err)
            } else {
                "Data transformation error".to_string()
            },
        }
    }
}

#[derive(Debug)]
pub struct ObjectNotFound(pub String);

impl From<&ObjectNotFound> for &str {
    fn from(_: &ObjectNotFound) -> Self {
        "ObjectNotFound"
    }
}

impl From<ObjectNotFound> for ApiError {
    fn from(err: ObjectNotFound) -> Self {
        Self {
            status: StatusCode::NOT_FOUND,
            code: err.error_code(),
            message: if cfg!(debug_assertions) {
                format!("Object not found: {}", err.0)
            } else {
                "Object not found".to_string()
            },
        }
    }
}

#[derive(Debug)]
pub struct ArgumentOutOfRange(pub String);

impl From<&ArgumentOutOfRange> for &str {
    fn from(_: &ArgumentOutOfRange) -> Self {
        "ArgumentOutOfRange"
    }
}

impl From<ArgumentOutOfRange> for ApiError {
    fn from(err: ArgumentOutOfRange) -> Self {
        Self {
            status: StatusCode::BAD_REQUEST,
            code: err.error_code(),
            message: format!("Argument out of range: {}", err.0),
        }
    }
}
