//! GraphQL SDL (Schema Definition Language) output backend.
//!
//! Emits a `.graphql` SDL document from the IR.
//!
//! Mapping:
//! - Structs → `type Foo { … }` (uses `input Foo { … }` when `use_input_types` is set)
//! - String-literal enums → `enum Status { PENDING ACTIVE DONE }`
//! - Int-literal enums → `enum` with named variants (names upper-cased; unnamed use `V<n>`)
//! - Tagged unions → `union Foo = Bar | Baz` (one type per variant; variant types are emitted
//!   as anonymous inline types named `<UnionName><VariantTag>`)
//! - Type aliases → a comment noting the alias; no native SDL equivalent
//!
//! Nullability:
//! - `required: true, nullable: false` → `FieldType!` (non-null)
//! - `required: false` or `nullable: true` → `FieldType` (nullable in GraphQL terms)

use crate::ir::{EnumKind, Field, Schema, Type, TypeDef, TypeDefKind};
use crate::traits::{Backend, BackendCategory};

/// Options for GraphQL SDL generation.
#[derive(Debug, Clone, Default)]
pub struct GraphQlOptions {
    /// When `true`, struct types are emitted as `input` types instead of `type`.
    /// Useful when the schema represents input-only objects.
    pub use_input_types: bool,
}

/// Generate a GraphQL SDL document from an IR schema.
pub fn generate_graphql_sdl(schema: &Schema, options: &GraphQlOptions) -> String {
    let mut out = String::new();
    out.push_str("# Auto-generated by normalize-typegen\n\n");

    // First pass: collect union variant helper types that need to be emitted.
    let mut extra_types: Vec<String> = Vec::new();

    for (i, def) in schema.definitions.iter().enumerate() {
        if i > 0 {
            out.push('\n');
        }
        generate_typedef(&mut out, def, options, &mut extra_types);
    }

    // Emit helper types for tagged union variants.
    for extra in &extra_types {
        out.push('\n');
        out.push_str(extra);
    }

    out
}

fn generate_typedef(
    out: &mut String,
    def: &TypeDef,
    options: &GraphQlOptions,
    extra: &mut Vec<String>,
) {
    if let Some(docs) = &def.docs {
        out.push_str("\"\"\"");
        out.push_str(docs);
        out.push_str("\"\"\"\n");
    }

    match &def.kind {
        TypeDefKind::Struct(s) => {
            let keyword = if options.use_input_types {
                "input"
            } else {
                "type"
            };
            out.push_str(keyword);
            out.push(' ');
            out.push_str(&def.name);
            out.push_str(" {\n");
            for field in &s.fields {
                generate_field(out, field);
            }
            out.push_str("}\n");
        }

        TypeDefKind::Enum(e) => match &e.kind {
            EnumKind::StringLiteral(variants) => {
                out.push_str("enum ");
                out.push_str(&def.name);
                out.push_str(" {\n");
                for variant in variants {
                    if let Some(docs) = &variant.docs {
                        out.push_str("  \"\"\"");
                        out.push_str(docs);
                        out.push_str("\"\"\"\n");
                    }
                    out.push_str("  ");
                    out.push_str(&variant.value.to_uppercase());
                    out.push('\n');
                }
                out.push_str("}\n");
            }
            EnumKind::IntLiteral(variants) => {
                out.push_str("enum ");
                out.push_str(&def.name);
                out.push_str(" {\n");
                for variant in variants {
                    if let Some(docs) = &variant.docs {
                        out.push_str("  \"\"\"");
                        out.push_str(docs);
                        out.push_str("\"\"\"\n");
                    }
                    let name = variant
                        .name
                        .as_deref()
                        .map(|n| n.to_uppercase())
                        .unwrap_or_else(|| format!("V{}", variant.value));
                    out.push_str("  ");
                    out.push_str(&name);
                    out.push('\n');
                }
                out.push_str("}\n");
            }
            EnumKind::Tagged(tagged) => {
                // Collect per-variant type names.
                let variant_type_names: Vec<String> = tagged
                    .variants
                    .iter()
                    .map(|v| format!("{}{}", def.name, to_pascal_case(&v.tag)))
                    .collect();

                out.push_str("union ");
                out.push_str(&def.name);
                out.push_str(" = ");
                out.push_str(&variant_type_names.join(" | "));
                out.push('\n');

                // Emit one helper type per variant.
                for (variant, type_name) in tagged.variants.iter().zip(&variant_type_names) {
                    let mut helper = String::new();
                    if let Some(docs) = &variant.docs {
                        helper.push_str("\"\"\"");
                        helper.push_str(docs);
                        helper.push_str("\"\"\"\n");
                    }
                    let keyword = if options.use_input_types {
                        "input"
                    } else {
                        "type"
                    };
                    helper.push_str(keyword);
                    helper.push(' ');
                    helper.push_str(type_name);
                    helper.push_str(" {\n");

                    // Discriminator field (always a non-null string).
                    helper.push_str("  ");
                    helper.push_str(&tagged.discriminator);
                    helper.push_str(": String!\n");

                    for field in &variant.fields {
                        let mut field_buf = String::new();
                        generate_field(&mut field_buf, field);
                        helper.push_str(&field_buf);
                    }
                    helper.push_str("}\n");
                    extra.push(helper);
                }
            }
        },

        TypeDefKind::Alias(ty) => {
            // GraphQL has no type aliases; emit a comment.
            out.push_str("# ");
            out.push_str(&def.name);
            out.push_str(" = ");
            out.push_str(&type_to_graphql(ty, true));
            out.push('\n');
        }
    }
}

fn generate_field(out: &mut String, field: &Field) {
    if let Some(docs) = &field.docs {
        out.push_str("  \"\"\"");
        out.push_str(docs);
        out.push_str("\"\"\"\n");
    }

    let inner_ty = match &field.ty {
        Type::Optional(inner) => inner.as_ref(),
        other => other,
    };

    // Non-null when required AND not nullable.
    let non_null = field.required && !field.nullable;
    let gql_ty = type_to_graphql(inner_ty, non_null);

    out.push_str("  ");
    out.push_str(&field.name);
    out.push_str(": ");
    out.push_str(&gql_ty);
    out.push('\n');
}

fn type_to_graphql(ty: &Type, non_null: bool) -> String {
    let bang = if non_null { "!" } else { "" };
    match ty {
        Type::String => format!("String{bang}"),
        Type::Integer { .. } => format!("Int{bang}"),
        Type::Float { .. } => format!("Float{bang}"),
        Type::Boolean => format!("Boolean{bang}"),
        Type::Null => "null".to_string(), // Not a real GraphQL type; best-effort.
        Type::Any => format!("String{bang}"), // Fallback — JSON has no Any scalar in SDL.
        Type::Array(inner) => format!("[{}]{bang}", type_to_graphql(inner, non_null)),
        Type::Map { .. } => format!("String{bang}"), // No native map in GraphQL; fallback.
        Type::Optional(inner) => type_to_graphql(inner, false), // Strip non-null.
        Type::Ref(name) => format!("{name}{bang}"),
        Type::Union(types) => {
            // GraphQL union syntax is at definition level; inline unions degrade to String.
            let _ = types;
            format!("String{bang}")
        }
        Type::StringLiteral(_) => format!("String{bang}"),
        Type::IntLiteral(_) => format!("Int{bang}"),
        Type::BoolLiteral(_) => format!("Boolean{bang}"),
    }
}

fn to_pascal_case(s: &str) -> String {
    let mut result = String::new();
    let mut cap = true;
    for c in s.chars() {
        if c == '_' || c == '-' || c == '.' {
            cap = true;
        } else if cap {
            // normalize-syntax-allow: rust/unwrap-in-impl - to_uppercase() always yields chars
            result.push(c.to_uppercase().next().unwrap());
            cap = false;
        } else {
            result.push(c);
        }
    }
    result
}

/// Static backend instance.
pub static GRAPHQL_BACKEND: GraphQlBackend = GraphQlBackend {
    options: GraphQlOptions {
        use_input_types: false,
    },
};

/// GraphQL SDL backend.
pub struct GraphQlBackend {
    /// Generation options.
    pub options: GraphQlOptions,
}

impl GraphQlBackend {
    pub fn new(options: GraphQlOptions) -> Self {
        Self { options }
    }
}

impl Backend for GraphQlBackend {
    fn name(&self) -> &'static str {
        "graphql"
    }

    fn language(&self) -> &'static str {
        "graphql"
    }

    fn extension(&self) -> &'static str {
        "graphql"
    }

    fn category(&self) -> BackendCategory {
        BackendCategory::Types
    }

    fn generate(&self, schema: &Schema) -> String {
        generate_graphql_sdl(schema, &self.options)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ir::{Field, TypeDef};

    #[test]
    fn simple_struct() {
        let mut schema = Schema::default();
        schema.add(TypeDef::structure(
            "User",
            vec![
                Field::required("id", Type::String),
                Field::optional("email", Type::String),
            ],
        ));
        let out = generate_graphql_sdl(&schema, &Default::default());
        assert!(out.contains("type User {"));
        assert!(out.contains("id: String!"));
        assert!(out.contains("email: String")); // nullable
        assert!(!out.contains("email: String!")); // not non-null
    }

    #[test]
    fn string_enum() {
        let mut schema = Schema::default();
        schema.add(TypeDef::string_enum("Status", vec!["pending", "active"]));
        let out = generate_graphql_sdl(&schema, &Default::default());
        assert!(out.contains("enum Status {"));
        assert!(out.contains("PENDING"));
        assert!(out.contains("ACTIVE"));
    }

    #[test]
    fn nullable_required_field() {
        let mut schema = Schema::default();
        schema.add(TypeDef::structure(
            "Obj",
            vec![Field::required("value", Type::String).nullable()],
        ));
        let out = generate_graphql_sdl(&schema, &Default::default());
        // nullable=true means field should NOT have !
        assert!(out.contains("value: String\n") || out.contains("value: String"));
        assert!(!out.contains("value: String!"));
    }

    #[test]
    fn input_types_option() {
        let mut schema = Schema::default();
        schema.add(TypeDef::structure("CreateUser", vec![]));
        let out = generate_graphql_sdl(
            &schema,
            &GraphQlOptions {
                use_input_types: true,
            },
        );
        assert!(out.contains("input CreateUser {"));
    }
}
