use std::fmt::{Error, Result as FmtResult, Write};

use async_graphql_value::ConstValue;

use crate::{
    Variables,
    parser::types::{
        ExecutableDocument, FragmentDefinition, OperationType, Selection, SelectionSet,
    },
    registry::{MetaInputValue, MetaType, MetaTypeName, Registry},
};

impl Registry {
    pub(crate) fn stringify_exec_doc(
        &self,
        variables: &Variables,
        doc: &ExecutableDocument,
    ) -> Result<String, Error> {
        let mut output = String::new();
        for (name, fragment) in &doc.fragments {
            self.stringify_fragment_definition(
                &mut output,
                variables,
                name,
                self.types
                    .get(fragment.node.type_condition.node.on.node.as_str()),
                &fragment.node,
            )?;
        }
        for (name, operation_definition) in doc.operations.iter() {
            write!(&mut output, "{} ", operation_definition.node.ty)?;
            if let Some(name) = name {
                write!(&mut output, "{}", name)?;

                if !operation_definition.node.variable_definitions.is_empty() {
                    output.push('(');
                    for (idx, variable_definition) in operation_definition
                        .node
                        .variable_definitions
                        .iter()
                        .enumerate()
                    {
                        if idx > 0 {
                            output.push_str(", ");
                        }
                        write!(
                            output,
                            "${}: {}",
                            variable_definition.node.name.node,
                            variable_definition.node.var_type.node
                        )?;
                        if let Some(default_value) = &variable_definition.node.default_value {
                            write!(output, " = {}", default_value.node)?;
                        }
                    }
                    output.push(')');
                }

                output.push(' ');
            }
            let root_type = match operation_definition.node.ty {
                OperationType::Query => self.types.get(&self.query_type),
                OperationType::Mutation => self
                    .mutation_type
                    .as_ref()
                    .and_then(|name| self.types.get(name)),
                OperationType::Subscription => self
                    .subscription_type
                    .as_ref()
                    .and_then(|name| self.types.get(name)),
            };
            self.stringify_selection_set(
                &mut output,
                variables,
                &operation_definition.node.selection_set.node,
                root_type,
            )?;
        }
        Ok(output)
    }

    fn stringify_fragment_definition(
        &self,
        output: &mut String,
        variables: &Variables,
        name: &str,
        parent_type: Option<&MetaType>,
        fragment_definition: &FragmentDefinition,
    ) -> FmtResult {
        write!(
            output,
            "fragment {} on {}",
            name, fragment_definition.type_condition.node.on.node
        )?;
        self.stringify_selection_set(
            output,
            variables,
            &fragment_definition.selection_set.node,
            parent_type,
        )?;
        output.push_str("}\n\n");
        Ok(())
    }

    fn stringify_input_value(
        &self,
        output: &mut String,
        meta_input_value: Option<&MetaInputValue>,
        value: &ConstValue,
    ) -> FmtResult {
        if meta_input_value.map(|v| v.is_secret).unwrap_or_default() {
            output.push_str("\"<secret>\"");
            return Ok(());
        }

        match value {
            ConstValue::Object(obj) => {
                let parent_type = meta_input_value.and_then(|input_value| {
                    self.types
                        .get(MetaTypeName::concrete_typename(&input_value.ty))
                });
                if let Some(MetaType::InputObject { input_fields, .. }) = parent_type {
                    output.push('{');
                    for (idx, (key, value)) in obj.iter().enumerate() {
                        if idx > 0 {
                            output.push_str(", ");
                        }
                        write!(output, "{}: ", key)?;
                        self.stringify_input_value(output, input_fields.get(key.as_str()), value)?;
                    }
                    output.push('}');
                } else {
                    write!(output, "{}", value)?;
                }
            }
            _ => write!(output, "{}", value)?,
        }

        Ok(())
    }

    fn stringify_selection_set(
        &self,
        output: &mut String,
        variables: &Variables,
        selection_set: &SelectionSet,
        parent_type: Option<&MetaType>,
    ) -> FmtResult {
        output.push_str("{ ");
        for (idx, selection) in selection_set.items.iter().map(|s| &s.node).enumerate() {
            if idx > 0 {
                output.push(' ');
            }
            match selection {
                Selection::Field(field) => {
                    if let Some(alias) = &field.node.alias {
                        write!(output, "{}:", alias.node)?;
                    }
                    write!(output, "{}", field.node.name.node)?;
                    if !field.node.arguments.is_empty() {
                        output.push('(');
                        for (idx, (name, argument)) in field.node.arguments.iter().enumerate() {
                            let meta_input_value = parent_type
                                .and_then(|parent_type| {
                                    parent_type.field_by_name(field.node.name.node.as_str())
                                })
                                .and_then(|field| field.args.get(name.node.as_str()));
                            if idx > 0 {
                                output.push_str(", ");
                            }
                            write!(output, "{}: ", name)?;
                            let value = argument
                                .node
                                .clone()
                                .into_const_with(|name| variables.get(&name).cloned().ok_or(()))
                                .unwrap_or_default();
                            self.stringify_input_value(output, meta_input_value, &value)?;
                        }
                        output.push(')');
                    }
                    if !field.node.selection_set.node.items.is_empty() {
                        output.push(' ');
                        let parent_type = parent_type
                            .and_then(|ty| ty.field_by_name(field.node.name.node.as_str()))
                            .and_then(|field| {
                                self.types.get(MetaTypeName::concrete_typename(&field.ty))
                            });
                        self.stringify_selection_set(
                            output,
                            variables,
                            &field.node.selection_set.node,
                            parent_type,
                        )?;
                    }
                }
                Selection::FragmentSpread(fragment_spread) => {
                    write!(output, "... {}", fragment_spread.node.fragment_name.node)?;
                }
                Selection::InlineFragment(inline_fragment) => {
                    output.push_str("... ");
                    let parent_type = if let Some(name) = &inline_fragment.node.type_condition {
                        write!(output, "on {} ", name.node.on.node)?;
                        self.types.get(name.node.on.node.as_str())
                    } else {
                        None
                    };
                    self.stringify_selection_set(
                        output,
                        variables,
                        &inline_fragment.node.selection_set.node,
                        parent_type,
                    )?;
                }
            }
        }
        output.push_str(" }");
        Ok(())
    }
}

#[cfg(test)]
#[allow(clippy::diverging_sub_expression)]
mod tests {
    use super::*;
    use crate::{parser::parse_query, *};

    #[test]
    fn test_stringify() {
        let registry = Registry::default();
        let doc = parse_query(
            r#"
            query Abc {
              a b c(a:1,b:2) {
                d e f
              }
            }
        "#,
        )
        .unwrap();
        assert_eq!(
            registry
                .stringify_exec_doc(&Default::default(), &doc)
                .unwrap(),
            r#"query Abc { a b c(a: 1, b: 2) { d e f } }"#
        );

        let doc = parse_query(
            r#"
            query Abc($a:Int) {
              value(input:$a)
            }
        "#,
        )
        .unwrap();
        assert_eq!(
            registry
                .stringify_exec_doc(
                    &Variables::from_value(value! ({
                        "a": 10,
                    })),
                    &doc
                )
                .unwrap(),
            r#"query Abc($a: Int) { value(input: 10) }"#
        );
    }

    #[test]
    fn test_stringify_secret() {
        #[derive(InputObject)]
        #[graphql(internal)]
        struct MyInput {
            v1: i32,
            #[graphql(secret)]
            v2: i32,
            v3: MyInput2,
        }

        #[derive(InputObject)]
        #[graphql(internal)]
        struct MyInput2 {
            v4: i32,
            #[graphql(secret)]
            v5: i32,
        }

        struct Query;

        #[Object(internal)]
        #[allow(unreachable_code, unused_variables)]
        impl Query {
            async fn value(&self, a: i32, #[graphql(secret)] b: i32, c: MyInput) -> i32 {
                todo!()
            }
        }

        let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
        let registry = schema.registry();
        let s = registry
            .stringify_exec_doc(
                &Default::default(),
                &parse_query(
                    r#"
            {
                value(a: 10, b: 20, c: { v1: 1, v2: 2, v3: { v4: 4, v5: 5}})
            }
        "#,
                )
                .unwrap(),
            )
            .unwrap();
        assert_eq!(
            s,
            r#"query { value(a: 10, b: "<secret>", c: {v1: 1, v2: "<secret>", v3: {v4: 4, v5: "<secret>"}}) }"#
        );
    }
}
