
use crate::registry::SchemaRegistry;
use crate::types::{
    EnumDefinition, EnumRepresentation, PrimitiveType, SchemaDefinition, SchemaType,
    StructDefinition, StructField, VariantData,
};
use serde_json::{json, Map, Value};

pub fn generate(definition: &SchemaDefinition) -> String {
    let value = generate_value(definition);
    serde_yaml::to_string(&value).unwrap_or_else(|_| "{}".to_string())
}

pub fn generate_value(definition: &SchemaDefinition) -> Value {
    let mut schema = generate_type_schema(&definition.schema_type);

    if let Value::Object(ref mut map) = schema {
        if let Some(ref desc) = definition.description {
            map.insert("description".to_string(), json!(desc));
        }

        if let Some(ref format) = definition.format {
            map.insert("format".to_string(), json!(format));
        }

        if definition.deprecated {
            map.insert("deprecated".to_string(), json!(true));
        }

        if !definition.examples.is_empty() {
            map.insert("examples".to_string(), json!(definition.examples));
        }

        if let Some(ref default) = definition.default {
            map.insert("default".to_string(), default.clone());
        }
    }

    schema
}

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

    root.insert("openapi".to_string(), json!("3.1.0"));

    let mut info = Map::new();
    info.insert(
        "title".to_string(),
        json!(registry
            .config()
            .title
            .as_deref()
            .unwrap_or("API Schema")),
    );
    if let Some(ref description) = registry.config().description {
        info.insert("description".to_string(), json!(description));
    }
    info.insert(
        "version".to_string(),
        json!(registry.config().version.as_deref().unwrap_or("1.0.0")),
    );
    root.insert("info".to_string(), Value::Object(info));

    root.insert("paths".to_string(), json!({}));

    let mut schemas = Map::new();
    for (name, definition) in registry.definitions() {
        let schema = generate_definition_schema(definition);
        schemas.insert(name.to_string(), schema);
    }

    let mut components = Map::new();
    components.insert("schemas".to_string(), Value::Object(schemas));
    root.insert("components".to_string(), Value::Object(components));

    serde_yaml::to_string(&Value::Object(root)).unwrap_or_else(|_| "{}".to_string())
}

fn generate_definition_schema(definition: &SchemaDefinition) -> Value {
    let mut schema = generate_type_schema(&definition.schema_type);

    if let Value::Object(ref mut map) = schema {
        if let Some(ref desc) = definition.description {
            map.insert("description".to_string(), json!(desc));
        }

        if let Some(ref format) = definition.format {
            map.insert("format".to_string(), json!(format));
        }

        if definition.deprecated {
            map.insert("deprecated".to_string(), json!(true));
        }

        if !definition.examples.is_empty() {
            map.insert("examples".to_string(), json!(definition.examples));
        }

        if let Some(ref default) = definition.default {
            map.insert("default".to_string(), default.clone());
        }
    }

    schema
}

fn generate_type_schema(schema_type: &SchemaType) -> Value {
    match schema_type {
        SchemaType::Primitive(prim) => generate_primitive_schema(prim),
        SchemaType::Option(inner) => generate_option_schema(inner),
        SchemaType::Array(inner) => generate_array_schema(inner),
        SchemaType::Set(inner) => generate_set_schema(inner),
        SchemaType::Map(inner) => generate_map_schema(inner),
        SchemaType::Tuple(types) => generate_tuple_schema(types),
        SchemaType::Struct(def) => generate_struct_schema(def),
        SchemaType::Enum(def) => generate_enum_schema(def),
        SchemaType::Newtype(def) => {
            if def.transparent {
                generate_type_schema(&def.inner_type)
            } else {
                generate_type_schema(&def.inner_type)
            }
        }
        SchemaType::Reference(name) => {
            json!({ "$ref": format!("#/components/schemas/{}", name) })
        }
        SchemaType::Unit => json!({ "type": "null" }),
        SchemaType::Any => json!({}),
    }
}

fn generate_primitive_schema(prim: &PrimitiveType) -> Value {
    match prim {
        PrimitiveType::Bool => json!({ "type": "boolean" }),

        PrimitiveType::I8 => json!({
            "type": "integer",
            "format": "int8",
            "minimum": i8::MIN,
            "maximum": i8::MAX
        }),
        PrimitiveType::I16 => json!({
            "type": "integer",
            "format": "int16",
            "minimum": i16::MIN,
            "maximum": i16::MAX
        }),
        PrimitiveType::I32 => json!({
            "type": "integer",
            "format": "int32",
            "minimum": i32::MIN,
            "maximum": i32::MAX
        }),
        PrimitiveType::I64 => json!({
            "type": "integer",
            "format": "int64"
        }),
        PrimitiveType::I128 => json!({
            "type": "string",
            "description": "128-bit signed integer as string"
        }),
        PrimitiveType::Isize => json!({
            "type": "integer",
            "format": "int64"
        }),

        PrimitiveType::U8 => json!({
            "type": "integer",
            "format": "uint8",
            "minimum": 0,
            "maximum": u8::MAX
        }),
        PrimitiveType::U16 => json!({
            "type": "integer",
            "format": "uint16",
            "minimum": 0,
            "maximum": u16::MAX
        }),
        PrimitiveType::U32 => json!({
            "type": "integer",
            "format": "uint32",
            "minimum": 0,
            "maximum": u32::MAX
        }),
        PrimitiveType::U64 => json!({
            "type": "integer",
            "format": "uint64",
            "minimum": 0
        }),
        PrimitiveType::U128 => json!({
            "type": "string",
            "description": "128-bit unsigned integer as string"
        }),
        PrimitiveType::Usize => json!({
            "type": "integer",
            "format": "uint64",
            "minimum": 0
        }),

        PrimitiveType::F32 => json!({
            "type": "number",
            "format": "float"
        }),
        PrimitiveType::F64 => json!({
            "type": "number",
            "format": "double"
        }),

        PrimitiveType::Char => json!({
            "type": "string",
            "minLength": 1,
            "maxLength": 1
        }),
        PrimitiveType::String => json!({ "type": "string" }),

        PrimitiveType::Bytes => json!({
            "type": "string",
            "format": "byte"
        }),
    }
}

fn generate_option_schema(inner: &SchemaType) -> Value {
    let inner_schema = generate_type_schema(inner);

    json!({
        "oneOf": [
            inner_schema,
            { "type": "null" }
        ]
    })
}

fn generate_array_schema(inner: &SchemaType) -> Value {
    let items_schema = generate_type_schema(inner);
    json!({
        "type": "array",
        "items": items_schema
    })
}

fn generate_set_schema(inner: &SchemaType) -> Value {
    let items_schema = generate_type_schema(inner);
    json!({
        "type": "array",
        "items": items_schema,
        "uniqueItems": true
    })
}

fn generate_map_schema(value_type: &SchemaType) -> Value {
    let value_schema = generate_type_schema(value_type);
    json!({
        "type": "object",
        "additionalProperties": value_schema
    })
}

fn generate_tuple_schema(types: &[Box<SchemaType>]) -> Value {
    let items: Vec<Value> = types.iter().map(|t| generate_type_schema(t)).collect();

    json!({
        "type": "array",
        "prefixItems": items,
        "items": false,
        "minItems": types.len(),
        "maxItems": types.len()
    })
}

fn generate_struct_schema(def: &StructDefinition) -> Value {
    if def.is_tuple_struct {
        return generate_tuple_struct_schema(def);
    }

    let mut properties = Map::new();
    let mut required = Vec::new();

    for (name, field) in &def.fields {
        let field_schema = generate_field_schema(field);
        properties.insert(name.clone(), field_schema);

        if field.required {
            required.push(json!(name));
        }
    }

    let mut schema = json!({
        "type": "object",
        "properties": properties,
        "additionalProperties": false
    });

    if !required.is_empty() {
        schema["required"] = json!(required);
    }

    schema
}

fn generate_tuple_struct_schema(def: &StructDefinition) -> Value {
    let items: Vec<Value> = def
        .fields
        .values()
        .map(|f| generate_type_schema(&f.schema_type))
        .collect();

    json!({
        "type": "array",
        "prefixItems": items,
        "items": false,
        "minItems": items.len(),
        "maxItems": items.len()
    })
}

fn generate_field_schema(field: &StructField) -> Value {
    let mut schema = generate_type_schema(&field.schema_type);

    if let Value::Object(ref mut map) = schema {
        if let Some(ref desc) = field.description {
            map.insert("description".to_string(), json!(desc));
        }

        if let Some(ref format) = field.format {
            map.insert("format".to_string(), json!(format));
        }

        if field.deprecated {
            map.insert("deprecated".to_string(), json!(true));
        }

        if let Some(ref default) = field.default {
            map.insert("default".to_string(), default.clone());
        }

        if let Some(min) = field.min_length {
            map.insert("minLength".to_string(), json!(min));
        }
        if let Some(max) = field.max_length {
            map.insert("maxLength".to_string(), json!(max));
        }

        if let Some(min) = field.minimum {
            map.insert("minimum".to_string(), json!(min));
        }
        if let Some(max) = field.maximum {
            map.insert("maximum".to_string(), json!(max));
        }
        if let Some(min) = field.exclusive_minimum {
            map.insert("exclusiveMinimum".to_string(), json!(min));
        }
        if let Some(max) = field.exclusive_maximum {
            map.insert("exclusiveMaximum".to_string(), json!(max));
        }

        if let Some(ref pattern) = field.pattern {
            map.insert("pattern".to_string(), json!(pattern));
        }
    }

    schema
}

fn generate_enum_schema(def: &EnumDefinition) -> Value {
    if def.is_simple_enum() {
        let values: Vec<&str> = def.variants.iter().map(|v| v.name.as_str()).collect();
        return json!({
            "type": "string",
            "enum": values
        });
    }

    let schemas: Vec<Value> = def
        .variants
        .iter()
        .map(|variant| generate_variant_schema(variant, &def.representation))
        .collect();

    json!({ "oneOf": schemas })
}

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

    match &variant.data {
        VariantData::Unit => match representation {
            EnumRepresentation::External => json!({
                "type": "object",
                "properties": {
                    variant_name: { "type": "null" }
                },
                "required": [variant_name],
                "additionalProperties": false
            }),
            EnumRepresentation::Internal { tag } => json!({
                "type": "object",
                "properties": {
                    tag: { "const": variant_name }
                },
                "required": [tag],
                "additionalProperties": false
            }),
            EnumRepresentation::Adjacent { tag, content: _ } => json!({
                "type": "object",
                "properties": {
                    tag: { "const": variant_name }
                },
                "required": [tag],
                "additionalProperties": false
            }),
            EnumRepresentation::Untagged => json!({ "type": "null" }),
        },

        VariantData::Newtype(inner) => {
            let inner_schema = generate_type_schema(inner);
            match representation {
                EnumRepresentation::External => json!({
                    "type": "object",
                    "properties": {
                        variant_name: inner_schema
                    },
                    "required": [variant_name],
                    "additionalProperties": false
                }),
                EnumRepresentation::Internal { tag } => {
                    let mut schema = inner_schema;
                    if let Value::Object(ref mut map) = schema {
                        merge_properties(
                            map,
                            &json!({ tag: { "const": variant_name } }),
                        );
                        add_to_required(map, tag);
                    }
                    schema
                }
                EnumRepresentation::Adjacent { tag, content } => json!({
                    "type": "object",
                    "properties": {
                        tag: { "const": variant_name },
                        content: inner_schema
                    },
                    "required": [tag, content],
                    "additionalProperties": false
                }),
                EnumRepresentation::Untagged => inner_schema,
            }
        }

        VariantData::Tuple(types) => {
            let items: Vec<Value> = types.iter().map(|t| generate_type_schema(t)).collect();
            let tuple_schema = json!({
                "type": "array",
                "prefixItems": items,
                "items": false,
                "minItems": items.len(),
                "maxItems": items.len()
            });

            match representation {
                EnumRepresentation::External => json!({
                    "type": "object",
                    "properties": {
                        variant_name: tuple_schema
                    },
                    "required": [variant_name],
                    "additionalProperties": false
                }),
                EnumRepresentation::Internal { tag: _ } => {
                    json!({
                        "type": "object",
                        "properties": {
                            variant_name: tuple_schema
                        },
                        "required": [variant_name],
                        "additionalProperties": false
                    })
                }
                EnumRepresentation::Adjacent { tag, content } => json!({
                    "type": "object",
                    "properties": {
                        tag: { "const": variant_name },
                        content: tuple_schema
                    },
                    "required": [tag, content],
                    "additionalProperties": false
                }),
                EnumRepresentation::Untagged => tuple_schema,
            }
        }

        VariantData::Struct(fields) => {
            let mut properties = Map::new();
            let mut required = Vec::new();

            for (name, field) in fields {
                let field_schema = generate_field_schema(field);
                properties.insert(name.clone(), field_schema);

                if field.required {
                    required.push(name.clone());
                }
            }

            match representation {
                EnumRepresentation::External => {
                    let struct_schema = json!({
                        "type": "object",
                        "properties": properties,
                        "required": required,
                        "additionalProperties": false
                    });
                    json!({
                        "type": "object",
                        "properties": {
                            variant_name: struct_schema
                        },
                        "required": [variant_name],
                        "additionalProperties": false
                    })
                }
                EnumRepresentation::Internal { tag } => {
                    properties.insert(tag.clone(), json!({ "const": variant_name }));
                    required.push(tag.clone());

                    json!({
                        "type": "object",
                        "properties": properties,
                        "required": required,
                        "additionalProperties": false
                    })
                }
                EnumRepresentation::Adjacent { tag, content } => {
                    let struct_schema = json!({
                        "type": "object",
                        "properties": properties,
                        "required": required,
                        "additionalProperties": false
                    });
                    json!({
                        "type": "object",
                        "properties": {
                            tag: { "const": variant_name },
                            content: struct_schema
                        },
                        "required": [tag, content],
                        "additionalProperties": false
                    })
                }
                EnumRepresentation::Untagged => json!({
                    "type": "object",
                    "properties": properties,
                    "required": required,
                    "additionalProperties": false
                }),
            }
        }
    }
}

fn merge_properties(map: &mut Map<String, Value>, new_props: &Value) {
    if let Value::Object(new) = new_props {
        let properties = map
            .entry("properties".to_string())
            .or_insert_with(|| json!({}));
        if let Value::Object(ref mut props) = properties {
            for (k, v) in new {
                props.insert(k.clone(), v.clone());
            }
        }
    }
}

fn add_to_required(map: &mut Map<String, Value>, field: &str) {
    let required = map
        .entry("required".to_string())
        .or_insert_with(|| json!([]));
    if let Value::Array(ref mut arr) = required {
        arr.push(json!(field));
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::types::EnumVariant;

    #[test]
    fn test_primitive_bool() {
        let schema = generate_primitive_schema(&PrimitiveType::Bool);
        assert_eq!(schema["type"], "boolean");
    }

    #[test]
    fn test_primitive_i32() {
        let schema = generate_primitive_schema(&PrimitiveType::I32);
        assert_eq!(schema["type"], "integer");
        assert_eq!(schema["format"], "int32");
    }

    #[test]
    fn test_primitive_f64() {
        let schema = generate_primitive_schema(&PrimitiveType::F64);
        assert_eq!(schema["type"], "number");
        assert_eq!(schema["format"], "double");
    }

    #[test]
    fn test_primitive_string() {
        let schema = generate_primitive_schema(&PrimitiveType::String);
        assert_eq!(schema["type"], "string");
    }

    #[test]
    fn test_option_schema() {
        let schema = generate_option_schema(&SchemaType::Primitive(PrimitiveType::String));
        assert!(schema["oneOf"].is_array());
    }

    #[test]
    fn test_array_schema() {
        let schema = generate_array_schema(&SchemaType::Primitive(PrimitiveType::I32));
        assert_eq!(schema["type"], "array");
        assert_eq!(schema["items"]["type"], "integer");
    }

    #[test]
    fn test_set_schema() {
        let schema = generate_set_schema(&SchemaType::Primitive(PrimitiveType::String));
        assert_eq!(schema["type"], "array");
        assert_eq!(schema["uniqueItems"], true);
    }

    #[test]
    fn test_map_schema() {
        let schema = generate_map_schema(&SchemaType::Primitive(PrimitiveType::String));
        assert_eq!(schema["type"], "object");
        assert_eq!(schema["additionalProperties"]["type"], "string");
    }

    #[test]
    fn test_struct_schema() {
        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 schema = generate_struct_schema(&def);
        assert_eq!(schema["type"], "object");
        assert!(schema["properties"]["name"].is_object());
        assert!(schema["properties"]["age"].is_object());
    }

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

        let schema = generate_enum_schema(&def);
        assert_eq!(schema["type"], "string");
        assert!(schema["enum"].as_array().unwrap().contains(&json!("Active")));
    }

    #[test]
    fn test_reference_schema() {
        let schema = generate_type_schema(&SchemaType::Reference("User".to_string()));
        assert_eq!(schema["$ref"], "#/components/schemas/User");
    }

    #[test]
    fn test_field_constraints() {
        let field = StructField::new(SchemaType::Primitive(PrimitiveType::String), "email")
            .with_format("email")
            .with_length(Some(5), Some(255))
            .with_description("User email address");

        let schema = generate_field_schema(&field);
        assert_eq!(schema["format"], "email");
        assert_eq!(schema["minLength"], 5);
        assert_eq!(schema["maxLength"], 255);
        assert_eq!(schema["description"], "User email address");
    }

    #[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 schema = generate_value(&def);
        assert_eq!(schema["description"], "A user in the system");
        assert_eq!(schema["type"], "object");
    }
}
