use std::ops::Deref;

use apollo_compiler::Name;
use apollo_compiler::Node;
use apollo_compiler::ast::Value;
use apollo_compiler::schema::Directive;

use crate::bail;
use crate::error::FederationError;
use crate::error::SingleFederationError;
use crate::link::graphql_definition::BooleanOrVariable;

pub(crate) fn directive_optional_enum_argument(
    application: &Node<Directive>,
    name: &Name,
) -> Result<Option<Name>, FederationError> {
    match application.specified_argument_by_name(name) {
        Some(value) => match value.deref() {
            Value::Enum(name) => Ok(Some(name.clone())),
            Value::Null => Ok(None),
            _ => Err(SingleFederationError::Internal {
                message: format!(
                    "Argument \"{}\" of directive \"@{}\" must be an enum value.",
                    name, application.name
                ),
            }
            .into()),
        },
        None => Ok(None),
    }
}

pub(crate) fn directive_required_enum_argument(
    application: &Node<Directive>,
    name: &Name,
) -> Result<Name, FederationError> {
    directive_optional_enum_argument(application, name)?.ok_or_else(|| {
        SingleFederationError::Internal {
            message: format!(
                "Required argument \"{}\" of directive \"@{}\" was not present.",
                name, application.name
            ),
        }
        .into()
    })
}

pub(crate) fn directive_optional_string_argument<'doc>(
    application: &'doc Node<Directive>,
    name: &Name,
) -> Result<Option<&'doc str>, FederationError> {
    match application.specified_argument_by_name(name) {
        Some(value) => match value.deref() {
            Value::String(name) => Ok(Some(name)),
            Value::Null => Ok(None),
            _ => Err(SingleFederationError::Internal {
                message: format!(
                    "Argument \"{}\" of directive \"@{}\" must be a string.",
                    name, application.name
                ),
            }
            .into()),
        },
        None => Ok(None),
    }
}

pub(crate) fn directive_required_string_argument<'doc>(
    application: &'doc Node<Directive>,
    name: &Name,
) -> Result<&'doc str, FederationError> {
    directive_optional_string_argument(application, name)?.ok_or_else(|| {
        SingleFederationError::Internal {
            message: format!(
                "Required argument \"{}\" of directive \"@{}\" was not present.",
                name, application.name
            ),
        }
        .into()
    })
}

pub(crate) fn directive_optional_boolean_argument(
    application: &Node<Directive>,
    name: &Name,
) -> Result<Option<bool>, FederationError> {
    match application.specified_argument_by_name(name) {
        Some(value) => match value.deref() {
            Value::Boolean(value) => Ok(Some(*value)),
            Value::Null => Ok(None),
            _ => Err(SingleFederationError::Internal {
                message: format!(
                    "Argument \"{}\" of directive \"@{}\" must be a boolean.",
                    name, application.name
                ),
            }
            .into()),
        },
        None => Ok(None),
    }
}

pub(crate) fn directive_optional_variable_boolean_argument(
    application: &Node<Directive>,
    name: &Name,
) -> Result<Option<BooleanOrVariable>, FederationError> {
    match application.specified_argument_by_name(name) {
        Some(value) => match value.deref() {
            Value::Variable(name) => Ok(Some(BooleanOrVariable::Variable(name.clone()))),
            Value::Boolean(value) => Ok(Some(BooleanOrVariable::Boolean(*value))),
            Value::Null => Ok(None),
            _ => Err(FederationError::internal(format!(
                "Argument \"{}\" of directive \"@{}\" must be a boolean.",
                name, application.name
            ))),
        },
        None => Ok(None),
    }
}

pub(crate) fn directive_optional_list_argument<'a>(
    application: &'a Node<Directive>,
    name: &'_ Name,
) -> Result<Option<&'a [Node<Value>]>, FederationError> {
    match application.specified_argument_by_name(name) {
        None => Ok(None),
        Some(value) => match value.as_ref() {
            Value::Null => Ok(None),
            Value::List(values) => Ok(Some(values.as_slice())),
            _ => bail!(
                r#"Argument "{name}" of directive "@{}" must be a list."#,
                application.name
            ),
        },
    }
}
