use async_graphql::dynamic::ResolverContext;
use axum::{
    Json,
    http::StatusCode,
    response::{IntoResponse, Response},
};
use fetch::HttpClientError;
use openidconnect::{
    ConfigurationError, RequestTokenError, SignatureVerificationError, SigningError,
    StandardErrorResponse, core::CoreErrorResponseType,
};
use serde::{Deserialize, Serialize};
use snafu::prelude::*;

use crate::models::auth::AuthType;

#[derive(Debug, Snafu)]
#[snafu(visibility(pub(crate)))]
pub enum AuthError {
    #[snafu(display("Not support auth method"))]
    NotSupportAuthMethod {
        supported: Vec<AuthType>,
        current: AuthType,
    },
    #[snafu(display("Failed to find auth record"))]
    FindAuthRecordError,
    #[snafu(display("Invalid credentials"))]
    BasicInvalidCredentials,
    #[snafu(transparent)]
    OidcInitError {
        source: jwt_authorizer::error::InitError,
    },
    #[snafu(display("Invalid oidc provider meta client error: {source}"))]
    OidcProviderHttpClientError { source: HttpClientError },
    #[snafu(transparent)]
    OidcProviderMetaError {
        source: openidconnect::DiscoveryError<HttpClientError>,
    },
    #[snafu(display("Invalid oidc provider URL: {source}"))]
    OidcProviderUrlError { source: url::ParseError },
    #[snafu(display("Invalid oidc redirect URI: {source}"))]
    OidcRequestRedirectUriError {
        #[snafu(source)]
        source: url::ParseError,
    },
    #[snafu(display("Oidc request session not found or expired"))]
    OidcCallbackRecordNotFoundOrExpiredError,
    #[snafu(display("Invalid oidc request callback nonce"))]
    OidcInvalidNonceError,
    #[snafu(display("Invalid oidc request callback state"))]
    OidcInvalidStateError,
    #[snafu(display("Invalid oidc request callback code"))]
    OidcInvalidCodeError,
    #[snafu(transparent)]
    OidcCallbackTokenConfigurationError { source: ConfigurationError },
    #[snafu(transparent)]
    OidcRequestTokenError {
        source: RequestTokenError<HttpClientError, StandardErrorResponse<CoreErrorResponseType>>,
    },
    #[snafu(display("Invalid oidc id token"))]
    OidcInvalidIdTokenError,
    #[snafu(display("Invalid oidc access token"))]
    OidcInvalidAccessTokenError,
    #[snafu(transparent)]
    OidcSignatureVerificationError { source: SignatureVerificationError },
    #[snafu(transparent)]
    OidcSigningError { source: SigningError },
    #[snafu(transparent)]
    OidcJwtAuthError { source: jwt_authorizer::AuthError },
    #[snafu(display("Extra scopes {expected} do not match found scopes {found}"))]
    OidcExtraScopesMatchError { expected: String, found: String },
    #[snafu(display("Extra claim {key} does not match expected value {expected}, found {found}"))]
    OidcExtraClaimMatchError {
        key: String,
        expected: String,
        found: String,
    },
    #[snafu(display("Extra claim {claim} missing"))]
    OidcExtraClaimMissingError { claim: String },
    #[snafu(display("Audience {aud} missing"))]
    OidcAudMissingError { aud: String },
    #[snafu(display("Subject missing"))]
    OidcSubMissingError,
    #[snafu(display(
        "GraphQL permission denied since {context_path}{}{field}{}{column}: {}",
        (if field.is_empty() { "" } else { "." }),
        (if column.is_empty() { "" } else { "." }),
        source.message
    ))]
    GraphQLPermissionError {
        #[snafu(source(false))]
        source: Box<async_graphql::Error>,
        field: String,
        column: String,
        context_path: String,
    },
}

impl AuthError {
    pub fn from_graphql_subscribe_id_guard(
        source: async_graphql::Error,
        context: &ResolverContext,
        field_name: &str,
        column_name: &str,
    ) -> AuthError {
        AuthError::GraphQLPermissionError {
            source: Box::new(source),
            field: field_name.to_string(),
            column: column_name.to_string(),
            context_path: context
                .ctx
                .path_node
                .map(|p| p.to_string_vec().join(""))
                .unwrap_or_default(),
        }
    }
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AuthErrorResponse {
    pub success: bool,
    pub message: String,
}

impl From<AuthError> for AuthErrorResponse {
    fn from(value: AuthError) -> Self {
        AuthErrorResponse {
            success: false,
            message: value.to_string(),
        }
    }
}

impl IntoResponse for AuthError {
    fn into_response(self) -> Response {
        (
            StatusCode::UNAUTHORIZED,
            Json(AuthErrorResponse::from(self)),
        )
            .into_response()
    }
}
