//! Valibot schema output backend.
//!
//! Generates Valibot schemas for runtime validation.
//! Valibot is a lightweight alternative to Zod with tree-shakeable design.

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

/// Options for Valibot code generation.
#[derive(Debug, Clone, Default)]
pub struct ValibotOptions {
    /// Whether to export all schemas.
    pub export: bool,
    /// Whether to also generate inferred type exports.
    pub infer_types: bool,
}

/// Generate Valibot schemas from an IR schema.
pub fn generate_valibot(schema: &Schema, options: &ValibotOptions) -> String {
    let mut out = String::new();
    out.push_str("// Auto-generated by normalize-typegen\n");

    // Collect needed imports
    let mut imports = vec!["object", "string", "number", "boolean", "array"];
    let has_optional = schema.definitions.iter().any(|d| match &d.kind {
        TypeDefKind::Struct(s) => s.fields.iter().any(|f| !f.required),
        _ => false,
    });
    if has_optional {
        imports.push("optional");
    }

    let has_enums = schema
        .definitions
        .iter()
        .any(|d| matches!(&d.kind, TypeDefKind::Enum(_)));
    if has_enums {
        imports.push("picklist");
        imports.push("literal");
        imports.push("union");
        imports.push("variant");
    }

    let has_records = schema.definitions.iter().any(|d| {
        if let TypeDefKind::Struct(s) = &d.kind {
            s.fields.iter().any(|f| matches!(f.ty, Type::Map { .. }))
        } else {
            false
        }
    });
    if has_records {
        imports.push("record");
    }

    imports.sort();
    imports.dedup();

    out.push_str("import * as v from \"valibot\";\n\n");

    if options.infer_types {
        out.push_str("import type { InferOutput } from \"valibot\";\n\n");
    }

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

        if options.infer_types {
            out.push('\n');
            generate_inferred_type(&mut out, def, options);
        }
    }

    out
}

fn generate_schema_def(out: &mut String, def: &TypeDef, options: &ValibotOptions) {
    // Doc comment
    if let Some(docs) = &def.docs {
        out.push_str("/** ");
        out.push_str(docs);
        out.push_str(" */\n");
    }

    let export = if options.export { "export " } else { "" };

    match &def.kind {
        TypeDefKind::Struct(s) => {
            out.push_str(export);
            out.push_str("const ");
            out.push_str(&schema_name(&def.name));
            out.push_str(" = v.object({\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(export);
                out.push_str("const ");
                out.push_str(&schema_name(&def.name));
                out.push_str(" = v.picklist([");

                for (i, variant) in variants.iter().enumerate() {
                    if i > 0 {
                        out.push_str(", ");
                    }
                    out.push('"');
                    out.push_str(&variant.value);
                    out.push('"');
                }

                out.push_str("]);\n");
            }
            EnumKind::IntLiteral(variants) => {
                out.push_str(export);
                out.push_str("const ");
                out.push_str(&schema_name(&def.name));
                out.push_str(" = v.union([");

                for (i, variant) in variants.iter().enumerate() {
                    if i > 0 {
                        out.push_str(", ");
                    }
                    out.push_str("v.literal(");
                    out.push_str(&variant.value.to_string());
                    out.push(')');
                }

                out.push_str("]);\n");
            }
            EnumKind::Tagged(tagged) => {
                out.push_str(export);
                out.push_str("const ");
                out.push_str(&schema_name(&def.name));
                out.push_str(" = v.variant(\"");
                out.push_str(&tagged.discriminator);
                out.push_str("\", [\n");

                for variant in &tagged.variants {
                    out.push_str("  v.object({\n");
                    out.push_str("    ");
                    out.push_str(&tagged.discriminator);
                    out.push_str(": v.literal(\"");
                    out.push_str(&variant.tag);
                    out.push_str("\"),\n");

                    for field in &variant.fields {
                        out.push_str("    ");
                        generate_field_inline(out, field);
                        out.push('\n');
                    }

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

                out.push_str("]);\n");
            }
        },
        TypeDefKind::Alias(ty) => {
            out.push_str(export);
            out.push_str("const ");
            out.push_str(&schema_name(&def.name));
            out.push_str(" = ");
            out.push_str(&type_to_valibot(ty));
            out.push_str(";\n");
        }
    }
}

fn generate_inferred_type(out: &mut String, def: &TypeDef, options: &ValibotOptions) {
    let export = if options.export { "export " } else { "" };
    out.push_str(export);
    out.push_str("type ");
    out.push_str(&def.name);
    out.push_str(" = InferOutput<typeof ");
    out.push_str(&schema_name(&def.name));
    out.push_str(">;\n");
}

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

    out.push_str("  ");
    out.push_str(&field.name);
    out.push_str(": ");

    let valibot_type = type_to_valibot(&field.ty);
    if field.required {
        out.push_str(&valibot_type);
    } else {
        out.push_str("v.optional(");
        out.push_str(&valibot_type);
        out.push(')');
    }

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

fn generate_field_inline(out: &mut String, field: &Field) {
    out.push_str(&field.name);
    out.push_str(": ");

    let valibot_type = type_to_valibot(&field.ty);
    if field.required {
        out.push_str(&valibot_type);
    } else {
        out.push_str("v.optional(");
        out.push_str(&valibot_type);
        out.push(')');
    }

    out.push(',');
}

fn type_to_valibot(ty: &Type) -> String {
    match ty {
        Type::String => "v.string()".to_string(),
        Type::Integer { .. } => "v.pipe(v.number(), v.integer())".to_string(),
        Type::Float { .. } => "v.number()".to_string(),
        Type::Boolean => "v.boolean()".to_string(),
        Type::Null => "v.null()".to_string(),
        Type::Array(inner) => format!("v.array({})", type_to_valibot(inner)),
        Type::Map { value, .. } => format!("v.record(v.string(), {})", type_to_valibot(value)),
        Type::Optional(inner) => format!("v.optional({})", type_to_valibot(inner)),
        Type::Ref(name) => schema_name(name),
        Type::Union(types) => {
            if types.len() == 1 {
                return type_to_valibot(&types[0]);
            }
            let parts: Vec<_> = types.iter().map(type_to_valibot).collect();
            format!("v.union([{}])", parts.join(", "))
        }
        Type::StringLiteral(s) => format!("v.literal(\"{}\")", s),
        Type::IntLiteral(i) => format!("v.literal({})", i),
        Type::BoolLiteral(b) => format!("v.literal({})", b),
        Type::Any => "v.unknown()".to_string(),
    }
}

fn schema_name(type_name: &str) -> String {
    format!("{}Schema", type_name)
}

/// Static backend instance with default options.
pub static VALIBOT_BACKEND: ValibotBackend = ValibotBackend {
    options: ValibotOptions {
        export: true,
        infer_types: true,
    },
};

/// Valibot backend with configurable options.
pub struct ValibotBackend {
    /// Generation options.
    pub options: ValibotOptions,
}

impl ValibotBackend {
    /// Create a new Valibot backend with the given options.
    pub fn new(options: ValibotOptions) -> Self {
        Self { options }
    }
}

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

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

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

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

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

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

    #[test]
    fn generate_simple_object() {
        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_valibot(&schema, &ValibotOptions::default());

        assert!(output.contains("import * as v from \"valibot\""));
        assert!(output.contains("const UserSchema = v.object("));
        assert!(output.contains("id: v.string(),"));
        assert!(output.contains("name: v.string(),"));
        assert!(output.contains("email: v.optional(v.string()),"));
    }

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

        let output = generate_valibot(&schema, &ValibotOptions::default());

        assert!(
            output.contains("const StatusSchema = v.picklist([\"pending\", \"active\", \"done\"])")
        );
    }

    #[test]
    fn generate_with_inferred_types() {
        let mut schema = Schema::default();
        schema.add(TypeDef::structure("User", vec![]));

        let output = generate_valibot(
            &schema,
            &ValibotOptions {
                export: true,
                infer_types: true,
            },
        );

        assert!(output.contains("import type { InferOutput } from \"valibot\""));
        assert!(output.contains("export const UserSchema = v.object("));
        assert!(output.contains("export type User = InferOutput<typeof UserSchema>"));
    }
}
