use thiserror::Error;

pub const RESPONSE_TRACE_ID_HEADER: &str = "x-athena-trace-id";
const REQUEST_ID_HEADER: &str = "x-request-id";
const PROXY_REQUEST_ID_HEADER: &str = "x-athena-proxy-request-id";

/// Authentication framework error types.
///
/// Each variant maps to an HTTP status code via [`AuthError::status_code`].
/// Use [`AuthError::into_response`] to produce a standardized JSON response
/// matching the better-auth OpenAPI spec: `{ "message": "..." }`.
#[derive(Error, Debug)]
pub enum AuthError {
    #[error("{0}")]
    BadRequest(String),

    #[error("Invalid request: {0}")]
    InvalidRequest(String),

    #[error("Validation error: {0}")]
    Validation(String),

    #[error("Invalid credentials")]
    InvalidCredentials,

    #[error("Authentication required")]
    Unauthenticated,

    #[error("Session not found or expired")]
    SessionNotFound,

    #[error("{0}")]
    Forbidden(String),

    #[error("Insufficient permissions")]
    Unauthorized,

    #[error("User not found")]
    UserNotFound,

    #[error("{0}")]
    NotFound(String),

    #[error("{0}")]
    Conflict(String),

    #[error("Too many requests")]
    RateLimited,

    #[error("{0}")]
    NotImplemented(String),

    #[error("Configuration error: {0}")]
    Config(String),

    #[error("Database error: {0}")]
    Database(#[from] DatabaseError),

    #[error("Serialization error: {0}")]
    Serialization(#[from] serde_json::Error),

    #[error("Plugin error: {plugin} - {message}")]
    Plugin { plugin: String, message: String },

    #[error("Internal server error: {0}")]
    Internal(String),

    #[error("Password hashing error: {0}")]
    PasswordHash(String),

    #[error("JWT error: {0}")]
    Jwt(#[from] jsonwebtoken::errors::Error),
}

impl AuthError {
    /// HTTP status code for this error.
    pub fn status_code(&self) -> u16 {
        match self {
            // 400
            Self::BadRequest(_) | Self::InvalidRequest(_) | Self::Validation(_) => 400,
            // 401
            Self::InvalidCredentials | Self::Unauthenticated | Self::SessionNotFound => 401,
            // 403
            Self::Forbidden(_) | Self::Unauthorized => 403,
            // 404
            Self::UserNotFound | Self::NotFound(_) => 404,
            // 409
            Self::Conflict(_) => 409,
            // 429
            Self::RateLimited => 429,
            // 501
            Self::NotImplemented(_) => 501,
            // 500
            Self::Config(_)
            | Self::Database(_)
            | Self::Serialization(_)
            | Self::Plugin { .. }
            | Self::Internal(_)
            | Self::PasswordHash(_)
            | Self::Jwt(_) => 500,
        }
    }

    /// Convert this error into a standardized [`AuthResponse`] matching the
    /// better-auth OpenAPI spec plus Athena diagnostics:
    /// `{ "message": "...", "version": "...", "traceId": "..." }`.
    ///
    /// Internal errors (500) use a generic message to avoid leaking details.
    pub fn into_response(self) -> crate::types::AuthResponse {
        self.into_response_with_trace_id(
            env!("CARGO_PKG_VERSION"),
            uuid::Uuid::new_v4().to_string(),
        )
    }

    pub fn into_response_with_trace_id(
        self,
        version: impl Into<String>,
        trace_id: impl Into<String>,
    ) -> crate::types::AuthResponse {
        let trace_id = trace_id.into();
        let status = self.status_code();
        let message = match status {
            500 => "Internal server error".to_string(),
            _ => self.to_string(),
        };

        let body =
            crate::types::ErrorMessageResponse::new(message.clone(), version, trace_id.clone());

        crate::types::AuthResponse::json(status, &body)
            .map(|response| normalize_auth_error_response_trace_id(response, trace_id.as_str()))
            .unwrap_or_else(|_| {
                crate::types::AuthResponse::text(status, &message)
                    .with_header(RESPONSE_TRACE_ID_HEADER, trace_id.clone())
                    .with_header(REQUEST_ID_HEADER, trace_id)
            })
    }

    pub fn bad_request(message: impl Into<String>) -> Self {
        Self::BadRequest(message.into())
    }

    pub fn forbidden(message: impl Into<String>) -> Self {
        Self::Forbidden(message.into())
    }

    pub fn not_found(message: impl Into<String>) -> Self {
        Self::NotFound(message.into())
    }

    pub fn conflict(message: impl Into<String>) -> Self {
        Self::Conflict(message.into())
    }

    pub fn not_implemented(message: impl Into<String>) -> Self {
        Self::NotImplemented(message.into())
    }

    pub fn plugin(plugin: &str, message: impl Into<String>) -> Self {
        Self::Plugin {
            plugin: plugin.to_string(),
            message: message.into(),
        }
    }

    pub fn config(message: impl Into<String>) -> Self {
        Self::Config(message.into())
    }

    pub fn internal(message: impl Into<String>) -> Self {
        Self::Internal(message.into())
    }

    pub fn validation(message: impl Into<String>) -> Self {
        Self::Validation(message.into())
    }
}

#[derive(Error, Debug)]
pub enum DatabaseError {
    #[error("Connection error: {0}")]
    Connection(String),

    #[error("Query error: {0}")]
    Query(String),

    #[error("Migration error: {0}")]
    Migration(String),

    #[error("Constraint violation: {0}")]
    Constraint(String),

    #[error("Transaction error: {0}")]
    Transaction(String),
}

#[cfg(feature = "sqlx-postgres")]
impl From<sqlx::Error> for DatabaseError {
    fn from(err: sqlx::Error) -> Self {
        match err {
            sqlx::Error::Database(db_err) => {
                if db_err.is_unique_violation() {
                    DatabaseError::Constraint(db_err.to_string())
                } else {
                    DatabaseError::Query(db_err.to_string())
                }
            }
            sqlx::Error::PoolClosed => DatabaseError::Connection("Pool closed".to_string()),
            sqlx::Error::PoolTimedOut => DatabaseError::Connection("Pool timed out".to_string()),
            _ => DatabaseError::Query(err.to_string()),
        }
    }
}

#[cfg(feature = "sqlx-postgres")]
impl From<sqlx::Error> for AuthError {
    fn from(err: sqlx::Error) -> Self {
        AuthError::Database(DatabaseError::from(err))
    }
}

pub type AuthResult<T> = Result<T, AuthError>;

pub fn request_trace_id_from_auth_request(req: &crate::types::AuthRequest) -> String {
    req.header_ignore_ascii_case(PROXY_REQUEST_ID_HEADER)
        .or_else(|| req.header_ignore_ascii_case(REQUEST_ID_HEADER))
        .cloned()
        .unwrap_or_else(|| uuid::Uuid::new_v4().to_string())
}

fn response_has_json_content_type(response: &crate::types::AuthResponse) -> bool {
    response.headers.iter().any(|(name, value)| {
        name.eq_ignore_ascii_case("content-type")
            && value
                .split(';')
                .next()
                .is_some_and(|mime| mime.trim().eq_ignore_ascii_case("application/json"))
    })
}

pub fn normalize_auth_error_response_trace_id(
    mut response: crate::types::AuthResponse,
    trace_id: &str,
) -> crate::types::AuthResponse {
    if response.status < 400 {
        return response;
    }

    response
        .headers
        .insert(RESPONSE_TRACE_ID_HEADER.to_string(), trace_id.to_string());
    response
        .headers
        .insert(REQUEST_ID_HEADER.to_string(), trace_id.to_string());

    if response_has_json_content_type(&response)
        && let Ok(mut body) = serde_json::from_slice::<serde_json::Value>(&response.body)
        && let Some(object) = body.as_object_mut()
        && object.contains_key("traceId")
    {
        object.insert(
            "traceId".to_string(),
            serde_json::Value::String(trace_id.to_string()),
        );
        if let Ok(encoded) = serde_json::to_vec(&body) {
            response.body = encoded;
        }
    }

    response
}

#[cfg(feature = "axum")]
impl axum::response::IntoResponse for AuthError {
    fn into_response(self) -> axum::response::Response {
        if self.status_code() == 500 {
            tracing::error!(error = %self, "Internal server error (axum response)");
        }

        let auth_response = AuthError::into_response(self);
        let status = axum::http::StatusCode::from_u16(auth_response.status)
            .unwrap_or(axum::http::StatusCode::INTERNAL_SERVER_ERROR);
        let mut response =
            axum::response::Response::new(axum::body::Body::from(auth_response.body));
        *response.status_mut() = status;

        for (name, value) in auth_response.headers {
            if let (Ok(header_name), Ok(header_value)) = (
                axum::http::HeaderName::from_bytes(name.as_bytes()),
                axum::http::HeaderValue::from_str(&value),
            ) {
                response.headers_mut().insert(header_name, header_value);
            }
        }

        response
    }
}

/// Convert `validator::ValidationErrors` into a standardized error response body.
///
/// Returns a 422 response with `{ "code": "VALIDATION_ERROR", "message": "...", "errors": {...} }`.
pub fn validation_error_response(
    errors: &validator::ValidationErrors,
) -> crate::types::AuthResponse {
    let field_errors: std::collections::HashMap<&str, Vec<String>> = errors
        .field_errors()
        .into_iter()
        .map(|(field, errs)| {
            let messages: Vec<String> = errs
                .iter()
                .map(|e| {
                    e.message
                        .as_ref()
                        .map(|m| m.to_string())
                        .unwrap_or_else(|| format!("Invalid value for {}", field))
                })
                .collect();
            (field, messages)
        })
        .collect();

    let body = crate::types::ValidationErrorResponse {
        code: "VALIDATION_ERROR",
        message: "Validation failed",
        errors: field_errors,
    };

    crate::types::AuthResponse::json(422, &body)
        .unwrap_or_else(|_| crate::types::AuthResponse::text(422, "Validation failed"))
}

/// Validate a request body, returning a parsed + validated value or an error response.
pub fn validate_request_body<T>(
    req: &crate::types::AuthRequest,
) -> Result<T, crate::types::AuthResponse>
where
    T: serde::de::DeserializeOwned + validator::Validate,
{
    let value: T = req.body_as_json().map_err(|e| {
        crate::types::AuthResponse::json(
            400,
            &crate::types::ErrorMessageResponse::with_generated_trace_id(
                format!("Invalid JSON: {}", e),
                env!("CARGO_PKG_VERSION"),
            ),
        )
        .unwrap_or_else(|_| crate::types::AuthResponse::text(400, "Invalid JSON"))
    })?;

    value
        .validate()
        .map_err(|e| validation_error_response(&e))?;

    Ok(value)
}
