//! TypeScript type definitions output backend.

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

/// Options for TypeScript code generation.
#[derive(Debug, Clone, Default)]
pub struct TypeScriptOptions {
    /// Use `readonly` modifier on fields.
    pub readonly: bool,
    /// Style for optional fields: "question" (field?: T) or "union" (field: T | undefined).
    pub optional_style: OptionalStyle,
    /// Whether to export all types.
    pub export: bool,
}

#[derive(Debug, Clone, Default)]
pub enum OptionalStyle {
    #[default]
    Question,
    Union,
}

/// Generate TypeScript type definitions from an IR schema.
pub fn generate_typescript_types(schema: &Schema, options: &TypeScriptOptions) -> String {
    let mut out = String::new();
    out.push_str("// Auto-generated by normalize-typegen\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: &TypeScriptOptions) {
    // 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("interface ");
            out.push_str(&def.name);
            out.push_str(" {\n");

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

            out.push_str("}\n");
        }
        TypeDefKind::Enum(e) => match &e.kind {
            EnumKind::StringLiteral(variants) => {
                out.push_str(export);
                out.push_str("type ");
                out.push_str(&def.name);
                out.push_str(" =\n");

                for (i, variant) in variants.iter().enumerate() {
                    if i > 0 {
                        out.push('\n');
                    }
                    out.push_str("  | \"");
                    out.push_str(&variant.value);
                    out.push('"');
                }
                out.push_str(";\n");
            }
            EnumKind::IntLiteral(variants) => {
                out.push_str(export);
                out.push_str("type ");
                out.push_str(&def.name);
                out.push_str(" =\n");

                for (i, variant) in variants.iter().enumerate() {
                    if i > 0 {
                        out.push('\n');
                    }
                    out.push_str("  | ");
                    out.push_str(&variant.value.to_string());
                }
                out.push_str(";\n");
            }
            EnumKind::Tagged(tagged) => {
                out.push_str(export);
                out.push_str("type ");
                out.push_str(&def.name);
                out.push_str(" =\n");

                for (i, variant) in tagged.variants.iter().enumerate() {
                    if i > 0 {
                        out.push('\n');
                    }
                    out.push_str("  | {\n");
                    out.push_str("      ");
                    out.push_str(&tagged.discriminator);
                    out.push_str(": \"");
                    out.push_str(&variant.tag);
                    out.push_str("\";\n");

                    for field in &variant.fields {
                        out.push_str("      ");
                        generate_field_inline(out, field, options);
                        out.push('\n');
                    }
                    out.push_str("    }");
                }
                out.push_str(";\n");
            }
        },
        TypeDefKind::Alias(ty) => {
            out.push_str(export);
            out.push_str("type ");
            out.push_str(&def.name);
            out.push_str(" = ");
            out.push_str(&type_to_ts(ty));
            out.push_str(";\n");
        }
    }
}

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

    out.push_str("  ");
    if options.readonly {
        out.push_str("readonly ");
    }
    out.push_str(&field.name);

    if !field.required {
        match options.optional_style {
            OptionalStyle::Question => out.push('?'),
            OptionalStyle::Union => {}
        }
    }

    out.push_str(": ");

    let ty_str = type_to_ts(&field.ty);
    out.push_str(&ty_str);

    if !field.required
        && let OptionalStyle::Union = options.optional_style
    {
        out.push_str(" | undefined");
    }

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

fn generate_field_inline(out: &mut String, field: &Field, options: &TypeScriptOptions) {
    if options.readonly {
        out.push_str("readonly ");
    }
    out.push_str(&field.name);

    if !field.required {
        match options.optional_style {
            OptionalStyle::Question => out.push('?'),
            OptionalStyle::Union => {}
        }
    }

    out.push_str(": ");
    out.push_str(&type_to_ts(&field.ty));

    if !field.required
        && let OptionalStyle::Union = options.optional_style
    {
        out.push_str(" | undefined");
    }

    out.push(';');
}

fn type_to_ts(ty: &Type) -> String {
    match ty {
        Type::String => "string".to_string(),
        Type::Integer { .. } | Type::Float { .. } => "number".to_string(),
        Type::Boolean => "boolean".to_string(),
        Type::Null => "null".to_string(),
        Type::Array(inner) => format!("{}[]", type_to_ts(inner)),
        Type::Map { value, .. } => format!("Record<string, {}>", type_to_ts(value)),
        Type::Optional(inner) => format!("{} | undefined", type_to_ts(inner)),
        Type::Ref(name) => name.clone(),
        Type::Union(types) => {
            let parts: Vec<_> = types.iter().map(type_to_ts).collect();
            parts.join(" | ")
        }
        Type::StringLiteral(s) => format!("\"{}\"", s),
        Type::IntLiteral(i) => i.to_string(),
        Type::BoolLiteral(b) => b.to_string(),
        Type::Any => "unknown".to_string(),
    }
}

/// Static backend instance with default options.
pub static TYPESCRIPT_BACKEND: TypeScriptBackend = TypeScriptBackend {
    options: TypeScriptOptions {
        readonly: false,
        optional_style: OptionalStyle::Question,
        export: true,
    },
};

/// TypeScript backend with configurable options.
pub struct TypeScriptBackend {
    /// Generation options.
    pub options: TypeScriptOptions,
}

impl TypeScriptBackend {
    /// Create a new TypeScript backend with the given options.
    pub fn new(options: TypeScriptOptions) -> Self {
        Self { options }
    }
}

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

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

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

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

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

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

    #[test]
    fn generate_simple_interface() {
        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_typescript_types(&schema, &TypeScriptOptions::default());

        assert!(output.contains("interface User"));
        assert!(output.contains("id: string;"));
        assert!(output.contains("name: string;"));
        assert!(output.contains("email?: string;"));
    }

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

        let output = generate_typescript_types(&schema, &TypeScriptOptions::default());

        assert!(output.contains("type Status ="));
        assert!(output.contains("| \"pending\""));
        assert!(output.contains("| \"active\""));
        assert!(output.contains("| \"done\""));
    }

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

        let output = generate_typescript_types(
            &schema,
            &TypeScriptOptions {
                export: true,
                ..Default::default()
            },
        );

        assert!(output.contains("export interface User"));
    }

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

        let output = generate_typescript_types(
            &schema,
            &TypeScriptOptions {
                readonly: true,
                ..Default::default()
            },
        );

        assert!(output.contains("readonly id: string;"));
    }
}
