
use crate::registry::SchemaRegistry;
use crate::types::{
    EnumDefinition, EnumRepresentation, PrimitiveType, SchemaDefinition, SchemaType,
    StructDefinition, StructField, VariantData,
};
use std::fmt::Write;

pub fn generate(definition: &SchemaDefinition) -> String {
    let mut output = String::new();

    if let Some(ref desc) = definition.description {
        write_jsdoc(&mut output, desc, definition.deprecated, 0);
    } else if definition.deprecated {
        writeln!(output, "/** @deprecated */").unwrap();
    }

    let type_def = generate_type_definition(&definition.name, &definition.schema_type);
    output.push_str(&type_def);

    output
}

pub fn generate_bundle(registry: &SchemaRegistry) -> String {
    let mut output = String::new();

    writeln!(output, "/**").unwrap();
    writeln!(output, " * Auto-generated TypeScript type definitions").unwrap();
    if let Some(ref title) = registry.config().title {
        writeln!(output, " * {}", title).unwrap();
    }
    if let Some(ref description) = registry.config().description {
        writeln!(output, " * ").unwrap();
        writeln!(output, " * {}", description).unwrap();
    }
    writeln!(output, " */").unwrap();
    writeln!(output).unwrap();

    let definitions: Vec<_> = match registry.topological_sort() {
        Ok(sorted) => sorted,
        Err(_) => registry.definitions().map(|(_, def)| def).collect(),
    };

    for definition in definitions {
        if let Some(ref desc) = definition.description {
            write_jsdoc(&mut output, desc, definition.deprecated, 0);
        } else if definition.deprecated {
            writeln!(output, "/** @deprecated */").unwrap();
        }

        let type_def = generate_type_definition(&definition.name, &definition.schema_type);
        output.push_str(&type_def);
        writeln!(output).unwrap();
    }

    output
}

fn generate_type_definition(name: &str, schema_type: &SchemaType) -> String {
    match schema_type {
        SchemaType::Struct(def) => generate_interface(name, def),
        SchemaType::Enum(def) => generate_enum_type(name, def),
        SchemaType::Newtype(def) => {
            format!(
                "export type {} = {};\n",
                name,
                generate_type_expression(&def.inner_type)
            )
        }
        _ => {
            format!(
                "export type {} = {};\n",
                name,
                generate_type_expression(schema_type)
            )
        }
    }
}

fn generate_interface(name: &str, def: &StructDefinition) -> String {
    let mut output = String::new();

    if def.is_tuple_struct {
        let types: Vec<String> = def
            .fields
            .values()
            .map(|f| generate_type_expression(&f.schema_type))
            .collect();
        writeln!(output, "export type {} = [{}];", name, types.join(", ")).unwrap();
    } else {
        writeln!(output, "export interface {} {{", name).unwrap();

        for (field_name, field) in &def.fields {
            if field.description.is_some() || field.deprecated {
                write_field_jsdoc(&mut output, field);
            }

            let ts_name = sanitize_property_name(field_name);
            let optional = if field.required { "" } else { "?" };
            let type_expr = generate_type_expression(&field.schema_type);

            let type_with_null = if field.nullable && field.required {
                format!("{} | null", type_expr)
            } else {
                type_expr
            };

            writeln!(output, "  {}{}: {};", ts_name, optional, type_with_null).unwrap();
        }

        writeln!(output, "}}").unwrap();
    }

    output
}

fn generate_enum_type(name: &str, def: &EnumDefinition) -> String {
    let mut output = String::new();

    if def.is_simple_enum() {
        let values: Vec<String> = def
            .variants
            .iter()
            .map(|v| format!("\"{}\"", v.name))
            .collect();
        writeln!(output, "export type {} = {};", name, values.join(" | ")).unwrap();
        return output;
    }

    let variant_types: Vec<String> = def
        .variants
        .iter()
        .map(|variant| generate_variant_type(variant, &def.representation))
        .collect();

    writeln!(output, "export type {} = {};", name, variant_types.join(" | ")).unwrap();

    for variant in &def.variants {
        if let VariantData::Struct(fields) = &variant.data {
            let interface_name = format!("{}_{}", name, variant.name);
            output.push_str(&generate_variant_interface(&interface_name, fields));
        }
    }

    output
}

fn generate_variant_type(
    variant: &crate::types::EnumVariant,
    representation: &EnumRepresentation,
) -> String {
    let variant_name = &variant.name;

    match &variant.data {
        VariantData::Unit => match representation {
            EnumRepresentation::External => {
                format!("{{ {}: null }}", sanitize_property_name(variant_name))
            }
            EnumRepresentation::Internal { tag } => {
                format!("{{ {}: \"{}\" }}", tag, variant_name)
            }
            EnumRepresentation::Adjacent { tag, content: _ } => {
                format!("{{ {}: \"{}\" }}", tag, variant_name)
            }
            EnumRepresentation::Untagged => "null".to_string(),
        },

        VariantData::Newtype(inner) => {
            let inner_type = generate_type_expression(inner);
            match representation {
                EnumRepresentation::External => {
                    format!(
                        "{{ {}: {} }}",
                        sanitize_property_name(variant_name),
                        inner_type
                    )
                }
                EnumRepresentation::Internal { tag } => {
                    format!("{{ {}: \"{}\" }} & {}", tag, variant_name, inner_type)
                }
                EnumRepresentation::Adjacent { tag, content } => {
                    format!(
                        "{{ {}: \"{}\", {}: {} }}",
                        tag, variant_name, content, inner_type
                    )
                }
                EnumRepresentation::Untagged => inner_type,
            }
        }

        VariantData::Tuple(types) => {
            let tuple_types: Vec<String> =
                types.iter().map(|t| generate_type_expression(t)).collect();
            let tuple_type = format!("[{}]", tuple_types.join(", "));

            match representation {
                EnumRepresentation::External => {
                    format!(
                        "{{ {}: {} }}",
                        sanitize_property_name(variant_name),
                        tuple_type
                    )
                }
                EnumRepresentation::Internal { tag: _ } => {
                    format!(
                        "{{ {}: {} }}",
                        sanitize_property_name(variant_name),
                        tuple_type
                    )
                }
                EnumRepresentation::Adjacent { tag, content } => {
                    format!(
                        "{{ {}: \"{}\", {}: {} }}",
                        tag, variant_name, content, tuple_type
                    )
                }
                EnumRepresentation::Untagged => tuple_type,
            }
        }

        VariantData::Struct(fields) => {
            let mut props = Vec::new();

            match representation {
                EnumRepresentation::Internal { tag } => {
                    props.push(format!("{}: \"{}\"", tag, variant_name));
                }
                EnumRepresentation::Adjacent { tag, content } => {
                    let struct_fields = generate_struct_fields_inline(fields);
                    return format!(
                        "{{ {}: \"{}\", {}: {{ {} }} }}",
                        tag, variant_name, content, struct_fields
                    );
                }
                EnumRepresentation::External => {
                    let struct_fields = generate_struct_fields_inline(fields);
                    return format!(
                        "{{ {}: {{ {} }} }}",
                        sanitize_property_name(variant_name),
                        struct_fields
                    );
                }
                EnumRepresentation::Untagged => {}
            }

            for (field_name, field) in fields {
                let ts_name = sanitize_property_name(field_name);
                let optional = if field.required { "" } else { "?" };
                let type_expr = generate_type_expression(&field.schema_type);
                props.push(format!("{}{}: {}", ts_name, optional, type_expr));
            }

            format!("{{ {} }}", props.join(", "))
        }
    }
}

fn generate_variant_interface(
    name: &str,
    fields: &indexmap::IndexMap<String, StructField>,
) -> String {
    let mut output = String::new();
    writeln!(output, "export interface {} {{", name).unwrap();

    for (field_name, field) in fields {
        let ts_name = sanitize_property_name(field_name);
        let optional = if field.required { "" } else { "?" };
        let type_expr = generate_type_expression(&field.schema_type);
        writeln!(output, "  {}{}: {};", ts_name, optional, type_expr).unwrap();
    }

    writeln!(output, "}}").unwrap();
    output
}

fn generate_struct_fields_inline(fields: &indexmap::IndexMap<String, StructField>) -> String {
    let props: Vec<String> = fields
        .iter()
        .map(|(field_name, field)| {
            let ts_name = sanitize_property_name(field_name);
            let optional = if field.required { "" } else { "?" };
            let type_expr = generate_type_expression(&field.schema_type);
            format!("{}{}: {}", ts_name, optional, type_expr)
        })
        .collect();
    props.join(", ")
}

fn generate_type_expression(schema_type: &SchemaType) -> String {
    match schema_type {
        SchemaType::Primitive(prim) => generate_primitive_type(prim),
        SchemaType::Option(inner) => {
            format!("{} | null", generate_type_expression(inner))
        }
        SchemaType::Array(inner) => {
            let inner_type = generate_type_expression(inner);
            if inner_type.contains('|') || inner_type.contains('{') {
                format!("Array<{}>", inner_type)
            } else {
                format!("{}[]", inner_type)
            }
        }
        SchemaType::Set(inner) => {
            format!("Set<{}>", generate_type_expression(inner))
        }
        SchemaType::Map(value_type) => {
            format!("Record<string, {}>", generate_type_expression(value_type))
        }
        SchemaType::Tuple(types) => {
            let type_strs: Vec<String> =
                types.iter().map(|t| generate_type_expression(t)).collect();
            format!("[{}]", type_strs.join(", "))
        }
        SchemaType::Struct(def) => {
            let mut props = Vec::new();
            for (name, field) in &def.fields {
                let ts_name = sanitize_property_name(name);
                let optional = if field.required { "" } else { "?" };
                let type_expr = generate_type_expression(&field.schema_type);
                props.push(format!("{}{}: {}", ts_name, optional, type_expr));
            }
            format!("{{ {} }}", props.join("; "))
        }
        SchemaType::Enum(def) => {
            if def.is_simple_enum() {
                let values: Vec<String> = def
                    .variants
                    .iter()
                    .map(|v| format!("\"{}\"", v.name))
                    .collect();
                values.join(" | ")
            } else {
                let types: Vec<String> = def
                    .variants
                    .iter()
                    .map(|v| generate_variant_type(v, &def.representation))
                    .collect();
                types.join(" | ")
            }
        }
        SchemaType::Newtype(def) => generate_type_expression(&def.inner_type),
        SchemaType::Reference(name) => name.clone(),
        SchemaType::Unit => "void".to_string(),
        SchemaType::Any => "unknown".to_string(),
    }
}

fn generate_primitive_type(prim: &PrimitiveType) -> String {
    match prim {
        PrimitiveType::Bool => "boolean".to_string(),

        PrimitiveType::I8
        | PrimitiveType::I16
        | PrimitiveType::I32
        | PrimitiveType::I64
        | PrimitiveType::I128
        | PrimitiveType::Isize
        | PrimitiveType::U8
        | PrimitiveType::U16
        | PrimitiveType::U32
        | PrimitiveType::U64
        | PrimitiveType::U128
        | PrimitiveType::Usize => "number".to_string(),

        PrimitiveType::F32 | PrimitiveType::F64 => "number".to_string(),

        PrimitiveType::Char | PrimitiveType::String => "string".to_string(),

        PrimitiveType::Bytes => "Uint8Array".to_string(),
    }
}

fn write_jsdoc(output: &mut String, description: &str, deprecated: bool, indent: usize) {
    let indent_str = " ".repeat(indent);
    writeln!(output, "{}/**", indent_str).unwrap();
    for line in description.lines() {
        writeln!(output, "{} * {}", indent_str, line).unwrap();
    }
    if deprecated {
        writeln!(output, "{} * @deprecated", indent_str).unwrap();
    }
    writeln!(output, "{} */", indent_str).unwrap();
}

fn write_field_jsdoc(output: &mut String, field: &StructField) {
    write!(output, "  /**").unwrap();

    if let Some(ref desc) = field.description {
        write!(output, " {}", desc).unwrap();
    }

    if field.deprecated {
        write!(output, " @deprecated").unwrap();
    }

    writeln!(output, " */").unwrap();
}

fn sanitize_property_name(name: &str) -> String {
    let needs_quoting = name.is_empty()
        || name.starts_with(|c: char| c.is_ascii_digit())
        || name.contains(|c: char| !c.is_alphanumeric() && c != '_' && c != '$')
        || is_reserved_word(name);

    if needs_quoting {
        format!("\"{}\"", name.replace('\\', "\\\\").replace('"', "\\\""))
    } else {
        name.to_string()
    }
}

fn is_reserved_word(name: &str) -> bool {
    matches!(
        name,
        "break"
            | "case"
            | "catch"
            | "class"
            | "const"
            | "continue"
            | "debugger"
            | "default"
            | "delete"
            | "do"
            | "else"
            | "enum"
            | "export"
            | "extends"
            | "false"
            | "finally"
            | "for"
            | "function"
            | "if"
            | "import"
            | "in"
            | "instanceof"
            | "new"
            | "null"
            | "return"
            | "super"
            | "switch"
            | "this"
            | "throw"
            | "true"
            | "try"
            | "typeof"
            | "var"
            | "void"
            | "while"
            | "with"
            | "yield"
            | "let"
            | "static"
    )
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::types::{EnumVariant, NewtypeDefinition};
    use indexmap::IndexMap;

    #[test]
    fn test_primitive_types() {
        assert_eq!(generate_primitive_type(&PrimitiveType::Bool), "boolean");
        assert_eq!(generate_primitive_type(&PrimitiveType::I32), "number");
        assert_eq!(generate_primitive_type(&PrimitiveType::U64), "number");
        assert_eq!(generate_primitive_type(&PrimitiveType::F64), "number");
        assert_eq!(generate_primitive_type(&PrimitiveType::String), "string");
        assert_eq!(generate_primitive_type(&PrimitiveType::Bytes), "Uint8Array");
    }

    #[test]
    fn test_option_type() {
        let opt = SchemaType::Option(Box::new(SchemaType::Primitive(PrimitiveType::String)));
        assert_eq!(generate_type_expression(&opt), "string | null");
    }

    #[test]
    fn test_array_type() {
        let arr = SchemaType::Array(Box::new(SchemaType::Primitive(PrimitiveType::I32)));
        assert_eq!(generate_type_expression(&arr), "number[]");

        let complex_arr = SchemaType::Array(Box::new(SchemaType::Option(Box::new(
            SchemaType::Primitive(PrimitiveType::I32),
        ))));
        assert_eq!(generate_type_expression(&complex_arr), "Array<number | null>");
    }

    #[test]
    fn test_map_type() {
        let map = SchemaType::Map(Box::new(SchemaType::Primitive(PrimitiveType::String)));
        assert_eq!(generate_type_expression(&map), "Record<string, string>");
    }

    #[test]
    fn test_tuple_type() {
        let tuple = SchemaType::Tuple(vec![
            Box::new(SchemaType::Primitive(PrimitiveType::I32)),
            Box::new(SchemaType::Primitive(PrimitiveType::String)),
        ]);
        assert_eq!(generate_type_expression(&tuple), "[number, string]");
    }

    #[test]
    fn test_simple_struct() {
        let def = StructDefinition::new()
            .with_field(
                "name",
                StructField::new(SchemaType::Primitive(PrimitiveType::String), "name"),
            )
            .with_field(
                "age",
                StructField::new(
                    SchemaType::Option(Box::new(SchemaType::Primitive(PrimitiveType::U8))),
                    "age",
                ),
            );

        let output = generate_interface("User", &def);
        assert!(output.contains("export interface User {"));
        assert!(output.contains("name: string;"));
        assert!(output.contains("age?: number | null;"));
    }

    #[test]
    fn test_simple_enum() {
        let def = EnumDefinition::new(EnumRepresentation::External)
            .with_variant(EnumVariant::unit("Active"))
            .with_variant(EnumVariant::unit("Inactive"))
            .with_variant(EnumVariant::unit("Pending"));

        let output = generate_enum_type("Status", &def);
        assert!(output.contains("export type Status = \"Active\" | \"Inactive\" | \"Pending\";"));
    }

    #[test]
    fn test_complex_enum_external() {
        let mut fields = IndexMap::new();
        fields.insert(
            "reason".to_string(),
            StructField::new(SchemaType::Primitive(PrimitiveType::String), "reason"),
        );

        let def = EnumDefinition::new(EnumRepresentation::External)
            .with_variant(EnumVariant::unit("Active"))
            .with_variant(EnumVariant::struct_variant("Suspended", fields));

        let output = generate_enum_type("Status", &def);
        assert!(output.contains("{ Active: null }"));
        assert!(output.contains("Suspended:"));
        assert!(output.contains("reason: string"));
    }

    #[test]
    fn test_complex_enum_internal() {
        let mut fields = IndexMap::new();
        fields.insert(
            "reason".to_string(),
            StructField::new(SchemaType::Primitive(PrimitiveType::String), "reason"),
        );

        let def = EnumDefinition::new(EnumRepresentation::Internal {
            tag: "type".to_string(),
        })
        .with_variant(EnumVariant::unit("Active"))
        .with_variant(EnumVariant::struct_variant("Suspended", fields));

        let output = generate_enum_type("Status", &def);
        assert!(output.contains("type: \"Active\""));
        assert!(output.contains("type: \"Suspended\""));
    }

    #[test]
    fn test_reference_type() {
        let ref_type = SchemaType::Reference("User".to_string());
        assert_eq!(generate_type_expression(&ref_type), "User");
    }

    #[test]
    fn test_full_definition() {
        let def = SchemaDefinition::new(
            "User",
            SchemaType::Struct(
                StructDefinition::new().with_field(
                    "id",
                    StructField::new(SchemaType::Primitive(PrimitiveType::U64), "id"),
                ),
            ),
        )
        .with_description("A user in the system");

        let output = generate(&def);
        assert!(output.contains("/**"));
        assert!(output.contains("A user in the system"));
        assert!(output.contains("export interface User"));
    }

    #[test]
    fn test_deprecated_field() {
        let mut def = StructDefinition::new();
        let mut field = StructField::new(SchemaType::Primitive(PrimitiveType::String), "oldField");
        field.deprecated = true;
        def.fields.insert("oldField".to_string(), field);

        let output = generate_interface("Test", &def);
        assert!(output.contains("@deprecated"));
    }

    #[test]
    fn test_sanitize_property_name() {
        assert_eq!(sanitize_property_name("normalName"), "normalName");
        assert_eq!(sanitize_property_name("123starts"), "\"123starts\"");
        assert_eq!(sanitize_property_name("has-dash"), "\"has-dash\"");
        assert_eq!(sanitize_property_name("class"), "\"class\"");
    }

    #[test]
    fn test_newtype() {
        let newtype = NewtypeDefinition::new(SchemaType::Primitive(PrimitiveType::String));
        let def = SchemaDefinition::new("Email", SchemaType::Newtype(newtype));
        let output = generate(&def);
        assert!(output.contains("export type Email = string;"));
    }
}
