//! Go type definitions output backend.
//!
//! Generates Go structs with json tags.

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

/// Options for Go code generation.
#[derive(Debug, Clone, Default)]
pub struct GoOptions {
    /// Package name.
    pub package: String,
    /// Whether to add json tags.
    pub json_tags: bool,
    /// Whether to use pointer types for optional fields.
    pub pointer_optionals: bool,
    /// Whether to add omitempty to optional fields.
    pub omitempty: bool,
}

impl GoOptions {
    pub fn with_package(package: impl Into<String>) -> Self {
        Self {
            package: package.into(),
            json_tags: true,
            pointer_optionals: true,
            omitempty: true,
        }
    }
}

/// Generate Go type definitions from an IR schema.
pub fn generate_go_types(schema: &Schema, options: &GoOptions) -> String {
    let mut out = String::new();
    out.push_str("// Auto-generated by normalize-typegen\n\n");

    // Package declaration
    let package = if options.package.is_empty() {
        "types"
    } else {
        &options.package
    };
    out.push_str("package ");
    out.push_str(package);
    out.push_str("\n\n");

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

    out
}

fn generate_typedef(out: &mut String, def: &TypeDef, options: &GoOptions) {
    // Doc comment
    if let Some(docs) = &def.docs {
        out.push_str("// ");
        out.push_str(&def.name);
        out.push(' ');
        out.push_str(docs);
        out.push('\n');
    }

    match &def.kind {
        TypeDefKind::Struct(s) => {
            out.push_str("type ");
            out.push_str(&def.name);
            out.push_str(" struct {\n");

            for field in &s.fields {
                generate_field(out, field, options);
            }

            out.push_str("}\n");
        }
        TypeDefKind::Enum(e) => match &e.kind {
            EnumKind::StringLiteral(variants) => {
                // Go doesn't have string enums, use type alias + constants
                out.push_str("type ");
                out.push_str(&def.name);
                out.push_str(" string\n\n");

                out.push_str("const (\n");
                for variant in variants {
                    let const_name = format!("{}{}", def.name, to_pascal_case(&variant.value));
                    out.push('\t');
                    out.push_str(&const_name);
                    out.push(' ');
                    out.push_str(&def.name);
                    out.push_str(" = \"");
                    out.push_str(&variant.value);
                    out.push_str("\"\n");
                }
                out.push_str(")\n");
            }
            EnumKind::IntLiteral(variants) => {
                out.push_str("type ");
                out.push_str(&def.name);
                out.push_str(" int\n\n");

                out.push_str("const (\n");
                for (i, variant) in variants.iter().enumerate() {
                    let const_name = variant
                        .name
                        .clone()
                        .unwrap_or_else(|| format!("{}_{}", def.name, i));
                    out.push('\t');
                    out.push_str(&const_name);
                    out.push(' ');
                    out.push_str(&def.name);
                    out.push_str(" = ");
                    out.push_str(&variant.value.to_string());
                    out.push('\n');
                }
                out.push_str(")\n");
            }
            EnumKind::Tagged(tagged) => {
                // Generate interface for the union
                out.push_str("type ");
                out.push_str(&def.name);
                out.push_str(" interface {\n");
                out.push_str("\tis");
                out.push_str(&def.name);
                out.push_str("()\n");
                out.push_str("}\n\n");

                // Generate variant structs
                for variant in &tagged.variants {
                    let variant_name = format!("{}{}", def.name, to_pascal_case(&variant.tag));

                    if let Some(docs) = &variant.docs {
                        out.push_str("// ");
                        out.push_str(&variant_name);
                        out.push(' ');
                        out.push_str(docs);
                        out.push('\n');
                    }

                    out.push_str("type ");
                    out.push_str(&variant_name);
                    out.push_str(" struct {\n");

                    // Discriminator field
                    out.push('\t');
                    out.push_str(&to_pascal_case(&tagged.discriminator));
                    out.push_str(" string");
                    if options.json_tags {
                        out.push_str(" `json:\"");
                        out.push_str(&tagged.discriminator);
                        out.push_str("\"`");
                    }
                    out.push('\n');

                    // Other fields
                    for field in &variant.fields {
                        generate_field(out, field, options);
                    }

                    out.push_str("}\n\n");

                    // Implement interface
                    out.push_str("func (");
                    out.push_str(&variant_name);
                    out.push_str(") is");
                    out.push_str(&def.name);
                    out.push_str("() {}\n\n");
                }
            }
        },
        TypeDefKind::Alias(ty) => {
            out.push_str("type ");
            out.push_str(&def.name);
            out.push_str(" = ");
            out.push_str(&type_to_go(ty, options));
            out.push('\n');
        }
    }
}

fn generate_field(out: &mut String, field: &Field, options: &GoOptions) {
    // Doc comment
    if let Some(docs) = &field.docs {
        out.push_str("\t// ");
        out.push_str(docs);
        out.push('\n');
    }

    out.push('\t');
    out.push_str(&to_pascal_case(&field.name));
    out.push(' ');

    let go_type = type_to_go(&field.ty, options);
    if !field.required
        && options.pointer_optionals
        && !go_type.starts_with('*')
        && !go_type.starts_with("[]")
        && !go_type.starts_with("map[")
    {
        out.push('*');
    }
    out.push_str(&go_type);

    // JSON tag
    if options.json_tags {
        out.push_str(" `json:\"");
        out.push_str(&field.name);
        if !field.required && options.omitempty {
            out.push_str(",omitempty");
        }
        out.push_str("\"`");
    }

    out.push('\n');
}

fn type_to_go(ty: &Type, options: &GoOptions) -> String {
    match ty {
        Type::String => "string".to_string(),
        Type::Integer { bits, signed } => match (bits, signed) {
            (8, true) => "int8",
            (8, false) => "uint8",
            (16, true) => "int16",
            (16, false) => "uint16",
            (32, true) => "int32",
            (32, false) => "uint32",
            (64, true) => "int64",
            (64, false) => "uint64",
            _ => "int",
        }
        .to_string(),
        Type::Float { bits } => if *bits <= 32 { "float32" } else { "float64" }.to_string(),
        Type::Boolean => "bool".to_string(),
        Type::Null => "any".to_string(), // Go doesn't have null type
        Type::Array(inner) => format!("[]{}", type_to_go(inner, options)),
        Type::Map { value, .. } => format!("map[string]{}", type_to_go(value, options)),
        Type::Optional(inner) => {
            let inner_type = type_to_go(inner, options);
            if options.pointer_optionals
                && !inner_type.starts_with('*')
                && !inner_type.starts_with("[]")
                && !inner_type.starts_with("map[")
            {
                format!("*{}", inner_type)
            } else {
                inner_type
            }
        }
        Type::Ref(name) => name.clone(),
        Type::Union(_) => "any".to_string(), // Go doesn't have union types
        Type::StringLiteral(_) => "string".to_string(),
        Type::IntLiteral(_) => "int".to_string(),
        Type::BoolLiteral(_) => "bool".to_string(),
        Type::Any => "any".to_string(),
    }
}

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

/// Static backend instance with default options.
pub static GO_BACKEND: GoBackend = GoBackend {
    options: GoOptions {
        package: String::new(), // Will use "types" as default
        json_tags: true,
        pointer_optionals: true,
        omitempty: true,
    },
};

/// Go backend with configurable options.
pub struct GoBackend {
    /// Generation options.
    pub options: GoOptions,
}

impl GoBackend {
    /// Create a new Go backend with the given options.
    pub fn new(options: GoOptions) -> Self {
        Self { options }
    }
}

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

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

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

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

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

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

    #[test]
    fn generate_simple_struct() {
        let mut schema = Schema::default();
        schema.add(TypeDef::structure(
            "User",
            vec![
                Field::required("id", Type::String),
                Field::required("name", Type::String),
                Field::optional("email", Type::String),
            ],
        ));

        let output = generate_go_types(&schema, &GoOptions::with_package("models"));

        assert!(output.contains("package models"));
        assert!(output.contains("type User struct {"));
        assert!(output.contains("Id string `json:\"id\"`"));
        assert!(output.contains("Name string `json:\"name\"`"));
        assert!(output.contains("Email *string `json:\"email,omitempty\"`"));
    }

    #[test]
    fn generate_string_enum() {
        let mut schema = Schema::default();
        schema.add(TypeDef::string_enum(
            "Status",
            vec!["pending", "active", "done"],
        ));

        let output = generate_go_types(&schema, &GoOptions::with_package("models"));

        assert!(output.contains("type Status string"));
        assert!(output.contains("StatusPending Status = \"pending\""));
        assert!(output.contains("StatusActive Status = \"active\""));
        assert!(output.contains("StatusDone Status = \"done\""));
    }

    #[test]
    fn generate_without_tags() {
        let mut schema = Schema::default();
        schema.add(TypeDef::structure(
            "User",
            vec![Field::required("id", Type::String)],
        ));

        let output = generate_go_types(
            &schema,
            &GoOptions {
                package: "models".into(),
                json_tags: false,
                ..Default::default()
            },
        );

        assert!(output.contains("Id string"));
        assert!(!output.contains("`json:"));
    }
}
