use ahash::AHashSet;
use alef_codegen::builder::RustFileBuilder;
use alef_codegen::generators::trait_bridge::find_bridge_field;
use alef_codegen::generators::{self, AsyncPattern, RustBindingConfig};
use alef_codegen::type_mapper::TypeMapper;
use alef_core::backend::{Backend, BuildConfig, BuildDependency, Capabilities, GeneratedFile};
use alef_core::config::{Language, ResolvedCrateConfig, resolve_output_dir};
use alef_core::hash::{self, CommentStyle};
use alef_core::ir::{ApiSurface, FunctionDef, TypeDef, TypeRef};
use std::borrow::Cow;
use std::path::PathBuf;

pub struct ExtendrBackend;

impl ExtendrBackend {
    fn binding_config<'a>(core_import: &'a str, lossy_skip_types: &'a [String]) -> RustBindingConfig<'a> {
        RustBindingConfig {
            struct_attrs: &[],
            field_attrs: &[],
            struct_derives: &["Clone"],
            // #[extendr] on impl blocks registers the struct as an R class, which enables
            // the ToVectorValue trait bound required for returning struct types from #[extendr]
            // free functions and for extendr_module! `impl Type;` declarations.
            method_block_attr: Some("extendr"),
            constructor_attr: "",
            static_attr: None,
            function_attr: "#[extendr]",
            enum_attrs: &[],
            enum_derives: &["Clone", "PartialEq"],
            needs_signature: false,
            signature_prefix: "",
            signature_suffix: "",
            core_import,
            async_pattern: AsyncPattern::TokioBlockOn,
            has_serde: true,
            type_name_prefix: "",
            option_duration_on_defaults: false,
            opaque_type_names: &[],
            // The extendr backend uses a separate #[extendr] free-function kwargs constructor
            // (gen_extendr_kwargs_constructor) for R callers. The in-class impl-block `fn new()`
            // constructor is suppressed to avoid type conversion errors: extendr cannot convert
            // custom enum or struct parameters from Robj in the classic constructor signature.
            skip_impl_constructor: true,
            // R maps small ints (u8, u16, u32, i8, i16) to i32 and large ints (u64, usize,
            // isize) to f64. Cast these back to the core types in gen_lossy_binding_to_core_fields
            // so that method bodies that construct core structs compile correctly.
            cast_uints_to_i32: true,
            cast_large_ints_to_f64: true,
            // extendr's #[extendr] macro only generates TryFrom<&Robj> for &T, not for T
            // (owned). Free function parameters that are Named non-opaque structs must therefore
            // be declared as &T in the binding signature so extendr can extract them from Robj.
            named_non_opaque_params_by_ref: true,
            // Flat data enums are output-only: no From<BindingType> impl exists for them.
            // Skip these types in gen_lossy_binding_to_core_fields so method bodies that
            // construct core structs emit Default::default() for those fields instead of
            // attempting .clone().into() which would fail to compile.
            lossy_skip_types,
            serializable_opaque_type_names: &[],
            never_skip_cfg_field_names: &[],
        }
    }
}

impl TypeMapper for ExtendrBackend {
    fn primitive(&self, prim: &alef_core::ir::PrimitiveType) -> Cow<'static, str> {
        use alef_core::ir::PrimitiveType;
        match prim {
            PrimitiveType::Bool => Cow::Borrowed("bool"),
            PrimitiveType::U8
            | PrimitiveType::U16
            | PrimitiveType::U32
            | PrimitiveType::I8
            | PrimitiveType::I16
            | PrimitiveType::I32 => Cow::Borrowed("i32"),
            PrimitiveType::U64 | PrimitiveType::I64 | PrimitiveType::Usize | PrimitiveType::Isize => {
                Cow::Borrowed("f64")
            }
            PrimitiveType::F32 | PrimitiveType::F64 => Cow::Borrowed("f64"),
        }
    }

    fn json(&self) -> Cow<'static, str> {
        Cow::Borrowed("String")
    }

    fn error_wrapper(&self) -> &str {
        "Result"
    }
}

impl Backend for ExtendrBackend {
    fn name(&self) -> &str {
        "extendr"
    }

    fn language(&self) -> Language {
        Language::R
    }

    fn capabilities(&self) -> Capabilities {
        Capabilities {
            // R is single-threaded; async funcs are blocked on a per-call tokio runtime.
            supports_async: true,
            supports_classes: true,
            supports_enums: true,
            supports_option: true,
            supports_result: true,
            ..Capabilities::default()
        }
    }

    fn generate_bindings(&self, api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
        let core_import = config.core_import_name();
        // Compute flat data enum names first so binding_config and conversion config can use them.
        let flat_data_enum_names_vec: Vec<String> = api
            .enums
            .iter()
            .filter(|e| is_flat_data_enum(e))
            .map(|e| e.name.clone())
            .collect();
        // JSON-passthrough wrapper structs need an `impl Name;` entry in
        // `extendr_module!` so their `#[extendr] impl` block (with `default`,
        // `from_json`) is wired into the R package's class registry.
        let json_passthrough_enum_names_vec: Vec<String> = api
            .enums
            .iter()
            .filter(|e| is_json_passthrough_data_enum(e))
            .map(|e| e.name.clone())
            .collect();
        let cfg = Self::binding_config(&core_import, &flat_data_enum_names_vec);

        // Build adapter body map for method body substitution.
        let adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::R)?;

        let mut builder = RustFileBuilder::new().with_generated_header();
        builder.add_inner_attribute("allow(dead_code, unused_imports, unused_variables)");
        builder.add_inner_attribute("allow(clippy::too_many_arguments, clippy::let_unit_value, clippy::needless_borrow, clippy::map_identity, clippy::just_underscores_and_digits, clippy::unused_unit, clippy::unnecessary_cast, clippy::unwrap_or_default, clippy::derivable_impls, clippy::needless_borrows_for_generic_args, clippy::unnecessary_fallible_conversions)");
        builder.add_import("extendr_api::prelude::*");
        // HashMap is needed for fields of type HashMap<K, V> (extendr prelude does not re-export it)
        builder.add_import("std::collections::HashMap");
        // Use extendr's Result<T> type alias (Result<T, Error>) in generated #[extendr] functions
        builder.add_import("extendr_api::Result");

        // Import traits needed for trait method dispatch
        for trait_path in generators::collect_trait_imports(api) {
            builder.add_import(&trait_path);
        }

        // Custom module declarations
        let custom_mods = config.custom_modules.for_language(Language::R);
        for module in custom_mods {
            builder.add_item(&format!("pub mod {module};"));
        }

        let opaque_types: ahash::AHashSet<String> = api
            .types
            .iter()
            .filter(|t| t.is_opaque)
            .map(|t| t.name.clone())
            .collect();
        // Opaque types that use Rc internally cannot be wrapped in Arc for the extendr binding.
        // These types are excluded from struct generation and skipped as struct fields.
        // (Note: VisitorHandle was previously Rc<RefCell<...>> and therefore arc-incompatible;
        // it is now Arc<Mutex<...>> and no longer requires special treatment here, but the
        // detection heuristic — opaque + cfg-gated — is retained for other potential cases.)
        // We identify them as opaque types that are cfg-feature-gated — only the visitor
        // machinery (feature = "visitor") produces such types in html-to-markdown.
        let arc_incompatible_opaque: ahash::AHashSet<String> = api
            .types
            .iter()
            .filter(|t| t.is_opaque && t.cfg.is_some())
            .map(|t| t.name.clone())
            .collect();
        let arc_incompatible_opaque_vec: Vec<String> = arc_incompatible_opaque.iter().cloned().collect();
        let mutex_types: ahash::AHashSet<String> = api
            .types
            .iter()
            .filter(|t| t.is_opaque && generators::type_needs_mutex(t))
            .map(|t| t.name.clone())
            .collect();
        let enum_names: ahash::AHashSet<String> = api.enums.iter().map(|e| e.name.clone()).collect();

        // Helper: returns true if a TypeRef references any arc-incompatible opaque type.
        let references_arc_incompatible = |ty: &alef_core::ir::TypeRef| -> bool {
            arc_incompatible_opaque_vec.iter().any(|n| {
                matches!(ty, alef_core::ir::TypeRef::Named(name) if name == n)
                    || matches!(ty, alef_core::ir::TypeRef::Optional(inner) if matches!(inner.as_ref(), alef_core::ir::TypeRef::Named(name) if name == n))
            })
        };
        // Helper: returns true if a method references any arc-incompatible opaque type in params or return.
        let method_references_arc_incompatible = |m: &alef_core::ir::MethodDef| -> bool {
            references_arc_incompatible(&m.return_type) || m.params.iter().any(|p| references_arc_incompatible(&p.ty))
        };

        // Helper: returns true if a TypeRef is (or contains) an enum type.
        let references_enum = |ty: &alef_core::ir::TypeRef| -> bool {
            match ty {
                alef_core::ir::TypeRef::Named(n) => enum_names.contains(n.as_str()),
                alef_core::ir::TypeRef::Optional(inner) => {
                    matches!(inner.as_ref(), alef_core::ir::TypeRef::Named(n) if enum_names.contains(n.as_str()))
                }
                _ => false,
            }
        };

        // Helper: returns true if a TypeRef references a non-opaque struct type by value.
        // Extendr generates TryFrom<&Robj> for &T (reference) but NOT TryFrom<&Robj> for T
        // (owned). Method parameters that take non-opaque structs by value therefore cannot
        // be converted from an incoming Robj. Such parameters trigger "T: TryFrom<&Robj> not
        // satisfied" compile errors. We exclude methods with such parameters so they are
        // omitted from the #[extendr] impl block.
        let param_is_owned_struct = |ty: &alef_core::ir::TypeRef| -> bool {
            let is_non_opaque_struct =
                |n: &str| !opaque_types.contains(n) && !enum_names.contains(n) && !arc_incompatible_opaque.contains(n);
            match ty {
                alef_core::ir::TypeRef::Named(n) => is_non_opaque_struct(n),
                alef_core::ir::TypeRef::Optional(inner) => {
                    matches!(inner.as_ref(), alef_core::ir::TypeRef::Named(n) if is_non_opaque_struct(n))
                }
                _ => false,
            }
        };

        // A method is incompatible with extendr if:
        //   • it references arc-incompatible types (arc-wrapped, Rc-based), OR
        //   • its return type is an enum (enums don't implement ToVectorValue), OR
        //   • any parameter takes a non-opaque struct by value (TryFrom<&Robj> for owned T
        //     is not generated by #[extendr] on the struct definition).
        let method_references_enum = |m: &alef_core::ir::MethodDef| -> bool {
            references_enum(&m.return_type)
                || m.params
                    .iter()
                    .any(|p| references_enum(&p.ty) || param_is_owned_struct(&p.ty))
        };

        // Helper: returns true if a TypeRef requires a type that extendr cannot automatically
        // convert from/to Robj.
        //
        // Extendr CANNOT handle:
        //   • Vec<T> where T is a non-opaque, non-enum, non-arc-incompatible struct —
        //     there is no automatic "R list → Vec<ExternalPtr<T>>" conversion.
        //   • Option<Vec<T>> of the same category.
        //
        // Extendr CAN handle:
        //   • Bare T (non-opaque struct) — wrapped/unwrapped via ExternalPtr.
        //   • Option<T> (non-opaque struct) — same, with NULL for None.
        //   • Vec<primitive/String> — native R vector types.
        //   • Enum types — ExternalPtr-backed after #[extendr] on enum.
        //   • Opaque types (Arc<T> wrappers) — ExternalPtr-backed.
        let is_extendr_native_incompatible = |ty: &alef_core::ir::TypeRef| -> bool {
            // A Vec<T> element type is "struct-like" (non-opaque, non-enum, non-arc-incompatible)
            let is_vec_element_incompatible =
                |n: &str| !opaque_types.contains(n) && !enum_names.contains(n) && !arc_incompatible_opaque.contains(n);
            match ty {
                // Vec<StructType> — cannot convert from R list automatically.
                alef_core::ir::TypeRef::Vec(inner) => {
                    match inner.as_ref() {
                        // Vec<StructType> — incompatible
                        alef_core::ir::TypeRef::Named(n) if is_vec_element_incompatible(n) => true,
                        // Vec<Vec<_>> — nested vectors not supported by extendr
                        alef_core::ir::TypeRef::Vec(_) => true,
                        _ => false,
                    }
                }
                // Option<Vec<StructType>> — same.
                alef_core::ir::TypeRef::Optional(inner) => {
                    if let alef_core::ir::TypeRef::Vec(inner2) = inner.as_ref() {
                        match inner2.as_ref() {
                            // Option<Vec<StructType>> — incompatible
                            alef_core::ir::TypeRef::Named(n) if is_vec_element_incompatible(n) => true,
                            // Option<Vec<Vec<_>>> — nested vectors not supported by extendr
                            alef_core::ir::TypeRef::Vec(_) => true,
                            _ => false,
                        }
                    } else {
                        false
                    }
                }
                // Bare Named types and Option<Named> are ExternalPtr-backed — compatible.
                _ => false,
            }
        };

        // Types that cannot be registered as extendr classes because their fields use types
        // that extendr cannot convert (Vec<T> where T is a non-opaque non-enum struct, etc.).
        // These types are still generated as Rust structs (for From impls), but are excluded
        // from #[extendr] impl block generation and from extendr_module! registration.
        let extendr_incompatible_types: ahash::AHashSet<String> = api
            .types
            .iter()
            .filter(|t| !t.is_opaque && !t.is_trait)
            .filter(|t| t.fields.iter().any(|f| is_extendr_native_incompatible(&f.ty)))
            .map(|t| t.name.clone())
            .collect();

        // Types that appear as function/method parameters — these are "input types" that
        // callers construct. Only generate from_json for input types whose core counterparts
        // implement serde::Deserialize. Output-only types (result/metadata structs) are excluded
        // so we avoid generating from_json for types whose core doesn't impl Deserialize.
        let input_type_names: ahash::AHashSet<String> = {
            fn collect_named(ty: &alef_core::ir::TypeRef, set: &mut ahash::AHashSet<String>) {
                match ty {
                    alef_core::ir::TypeRef::Named(n) => {
                        set.insert(n.clone());
                    }
                    alef_core::ir::TypeRef::Optional(inner) => collect_named(inner, set),
                    alef_core::ir::TypeRef::Vec(inner) => collect_named(inner, set),
                    _ => {}
                }
            }
            let mut set = ahash::AHashSet::new();
            for func in &api.functions {
                for p in &func.params {
                    collect_named(&p.ty, &mut set);
                }
            }
            for typ in &api.types {
                for m in &typ.methods {
                    for p in &m.params {
                        collect_named(&p.ty, &mut set);
                    }
                }
            }
            set
        };

        // Import Arc when there are arc-compatible opaque types.
        let has_arc_compatible = opaque_types.iter().any(|n| !arc_incompatible_opaque.contains(n));
        if has_arc_compatible {
            builder.add_import("std::sync::Arc");
        }

        // Generate type bindings
        for typ in api.types.iter().filter(|typ| !typ.is_trait) {
            if typ.is_opaque {
                // Skip opaque types that cannot be wrapped in Arc (e.g. VisitorHandle with Rc).
                if arc_incompatible_opaque.contains(&typ.name) {
                    continue;
                }
                // Opaque types wrap the core type in Arc<T> and delegate methods to self.inner.
                // Applying #[extendr] to the struct generates TryFrom<&Robj> for &Foo and
                // From<Foo> for Robj via ExternalPtr — required for method dispatch.
                let opaque_struct = generators::gen_opaque_struct(typ, &cfg);
                builder.add_item(&format!("#[extendr]\n{opaque_struct}"));
                // Filter methods on opaque types that cannot be expressed in the extendr binding:
                //   • methods referencing arc-incompatible types (e.g. visitor() with VisitorHandle)
                //   • methods taking/returning enum types (ToVectorValue not implemented for enums)
                //   • methods taking non-opaque structs by value (TryFrom<&Robj> for owned T missing)
                let has_excluded_opaque_methods = typ
                    .methods
                    .iter()
                    .any(|m| method_references_arc_incompatible(m) || method_references_enum(m));
                let opaque_impl_typ: std::borrow::Cow<alef_core::ir::TypeDef> = if has_excluded_opaque_methods {
                    let filtered = alef_core::ir::TypeDef {
                        methods: typ
                            .methods
                            .iter()
                            .filter(|m| !method_references_arc_incompatible(m) && !method_references_enum(m))
                            .cloned()
                            .collect(),
                        ..typ.clone()
                    };
                    std::borrow::Cow::Owned(filtered)
                } else {
                    std::borrow::Cow::Borrowed(typ)
                };
                let impl_block = generators::gen_opaque_impl_block(
                    &opaque_impl_typ,
                    self,
                    &cfg,
                    &opaque_types,
                    &mutex_types,
                    &adapter_bodies,
                );
                if !impl_block.is_empty() {
                    builder.add_item(&impl_block);
                } else {
                    // extendr requires a #[extendr] impl block for every type listed in
                    // extendr_module! — even opaque types with no methods need one so the
                    // meta__TypeName function is generated by the #[extendr] proc-macro.
                    builder.add_item(&format!("#[extendr]\nimpl {} {{}}", typ.name));
                }
            } else {
                // If this type has fields referencing arc-incompatible opaque types, generate the
                // struct with those fields removed. They are skipped in the binding because the
                // opaque wrapper struct (e.g. VisitorHandle) was not generated.
                // Also filter methods that take or return enum types — extendr enums don't
                // automatically implement ToVectorValue so they cannot be used as extendr
                // method params/return types in non-opaque impl blocks.
                let has_excluded_fields = typ.fields.iter().any(|f| references_arc_incompatible(&f.ty));
                let has_excluded_methods = typ
                    .methods
                    .iter()
                    .any(|m| method_references_arc_incompatible(m) || method_references_enum(m));
                let struct_typ: std::borrow::Cow<alef_core::ir::TypeDef> =
                    if has_excluded_fields || has_excluded_methods {
                        let filtered = alef_core::ir::TypeDef {
                            fields: typ
                                .fields
                                .iter()
                                .filter(|f| !references_arc_incompatible(&f.ty))
                                .cloned()
                                .collect(),
                            methods: typ
                                .methods
                                .iter()
                                .filter(|m| !method_references_arc_incompatible(m) && !method_references_enum(m))
                                .cloned()
                                .collect(),
                            ..typ.clone()
                        };
                        std::borrow::Cow::Owned(filtered)
                    } else {
                        std::borrow::Cow::Borrowed(typ)
                    };

                // gen_struct already emits #[derive(Default)] for all structs.
                // Emitting gen_struct_default_impl here would produce a conflicting
                // `impl Default` compile error. The derive covers all types where
                // can_generate_default_impl is true (all field types implement Default).

                if extendr_incompatible_types.contains(&struct_typ.name) {
                    // This type has fields (e.g. Vec<HeaderMetadata>) that extendr cannot
                    // convert from/to Robj. Do NOT register it as an extendr class — the
                    // #[extendr] impl and extendr_module! entry are omitted. The struct is
                    // still generated so From impls compile. Callers receive these types
                    // serialised to R named lists by the custom types.rs module.
                    builder.add_item(&generators::gen_struct(&struct_typ, self, &cfg));
                } else {
                    // Applying #[extendr] to the struct generates the conversions needed
                    // for extendr class registration:
                    //   • impl TryFrom<&Robj> for &Foo  (ExternalPtr unwrap)
                    //   • impl TryFrom<&mut Robj> for &mut Foo
                    //   • impl From<Foo> for Robj        (ExternalPtr wrap)
                    // Without this the free-function kwargs constructors cannot return Foo
                    // (Robj::from requires From<Foo> for Robj) and method &self params
                    // cannot be extracted from the incoming Robj.
                    let struct_item = generators::gen_struct(&struct_typ, self, &cfg);
                    builder.add_item(&format!("#[extendr]\n{struct_item}"));
                    // The impl block uses the full struct_typ (with real fields) so that method
                    // bodies (e.g. gen_lossy_binding_to_core_fields) emit correct field
                    // assignments. Constructor generation is suppressed via skip_impl_constructor
                    // in the binding config — the kwargs free-function constructor handles R
                    // object creation instead.
                    // Build from_json method body when the type has serde + has_default.
                    // from_json lets R callers construct typed ExternalPtrs from JSON strings
                    // (e.g. ExtractionConfig$from_json(jsonlite::toJSON(list(...)))).
                    // Generate from_json only for input types (function/method parameters)
                    // whose core counterparts implement serde::Deserialize. Output-only types
                    // (metadata/result structs) are excluded: their core types may not impl
                    // Deserialize and callers never need to construct them from JSON.
                    let from_json_method = if struct_typ.has_default
                        && !struct_typ.fields.is_empty()
                        && input_type_names.contains(&struct_typ.name)
                    {
                        let type_name = &struct_typ.name;
                        let core_path = struct_typ.rust_path.replace('-', "_");
                        // Deserialize via the core type so its serde setup (enum string variants,
                        // #[serde(default)] on all fields, etc.) applies. The binding has
                        // From<CoreType> generated by alef so conversion is always available.
                        format!(
                            "    pub fn from_json(json: String) -> extendr_api::Result<{type_name}> {{\n        \
                             let core: {core_path} = serde_json::from_str(&json)\n            \
                             .map_err(|e| extendr_api::Error::Other(e.to_string()))?;\n        \
                             Ok(core.into())\n    \
                             }}\n"
                        )
                    } else {
                        String::new()
                    };

                    let impl_block =
                        generators::gen_impl_block(&struct_typ, self, &cfg, &adapter_bodies, &opaque_types);
                    if !impl_block.is_empty() {
                        // Inject from_json (if any) into the existing #[extendr] impl block
                        // before the closing `}`. extendr only allows one #[extendr] impl per type.
                        let final_impl = if from_json_method.is_empty() {
                            impl_block
                        } else if let Some(pos) = impl_block.rfind('}') {
                            format!("{}{}{}", &impl_block[..pos], &from_json_method, &impl_block[pos..])
                        } else {
                            impl_block
                        };
                        builder.add_item(&final_impl);
                    } else {
                        // extendr requires a #[extendr] impl block for every type listed in
                        // extendr_module! — without it, the module macro cannot register the type.
                        let empty_or_from_json = if from_json_method.is_empty() {
                            format!("#[extendr]\nimpl {} {{}}", struct_typ.name)
                        } else {
                            format!("#[extendr]\nimpl {} {{\n{}}}", struct_typ.name, from_json_method)
                        };
                        builder.add_item(&empty_or_from_json);
                    }
                    // Generate kwargs config constructor if type has Default.
                    if struct_typ.has_default && !struct_typ.fields.is_empty() {
                        let map_fn = |ty: &alef_core::ir::TypeRef| self.map_type(ty);
                        let config_fn =
                            alef_codegen::config_gen::gen_extendr_kwargs_constructor(&struct_typ, &map_fn, &enum_names);
                        builder.add_item(&config_fn);
                    }
                }
            }
        }

        // Generate enum bindings.
        for e in &api.enums {
            if is_flat_data_enum(e) {
                // Data enums with all-tuple variants become flat structs so bindings can
                // access variant data with dot notation (e.g. result$format$excel$sheet_count).
                // The #[extendr] attribute registers it as a class; the impl block satisfies
                // extendr_module! even though the struct has no methods.
                let flat_struct = gen_extendr_flat_data_enum_struct(e, self, &cfg);
                builder.add_item(&format!("#[extendr]\n{flat_struct}"));
                builder.add_item(&format!("#[extendr]\nimpl {} {{}}", e.name));
            } else if is_json_passthrough_data_enum(e) {
                // Tagged data enums with struct variants (e.g. `EmbeddingModelType`
                // with `Preset { name: String }`) cannot be expressed losslessly as
                // flat unit-only enums — the variant payload is dropped on round
                // trip. Generate a newtype struct whose serde representation defers
                // entirely to the core enum, so nested deserialization through parent
                // structs preserves the full tagged payload across the FFI boundary.
                builder.add_item(&gen_extendr_json_passthrough_enum_struct(e, &core_import));
            } else {
                builder.add_item(&generators::gen_enum(e, &cfg));
            }
        }

        // Emit binding↔core From impls so generated bodies can use `.into()` /
        // `Type::from(core)` to bridge between the extendr-facing binding types and
        // the core Rust types.  Without these impls the generated `convert` and
        // builder methods fail with E0277 unsatisfied trait bound errors.
        let binding_to_core = alef_codegen::conversions::convertible_types(api);
        let core_to_binding = alef_codegen::conversions::core_to_binding_convertible_types(api);
        let input_types = alef_codegen::conversions::input_type_names(api);
        // Flat data enums whose tuple variant data types are all primitive/String can round-trip.
        // Those that have complex output-only types (e.g. FormatMetadata → DocxMetadata) cannot.
        let non_round_trip_flat_enums: Vec<String> = api
            .enums
            .iter()
            .filter(|e| is_flat_data_enum(e) && !can_flat_data_enum_round_trip(e))
            .map(|e| e.name.clone())
            .collect();
        let extendr_conversion_cfg = alef_codegen::conversions::ConversionConfig {
            cast_uints_to_i32: true,
            cast_large_ints_to_f64: true,
            // Exclude arc-incompatible opaque types (e.g. VisitorHandle) from conversion
            // generation so that struct fields referencing them are skipped in From impls.
            exclude_types: &arc_incompatible_opaque_vec,
            // Only skip flat data enums that are output-only (complex variant data types).
            // Round-trip-safe ones (e.g. OutputFormat with only String data) have a
            // From<BindingStruct> for CoreEnum impl generated and don't need skipping.
            from_binding_skip_types: &non_round_trip_flat_enums,
            // The extendr binding crate doesn't carry kreuzberg feature flags into its
            // own Cargo.toml, so cfg-gated core fields are dropped from the binding struct
            // (see `gen_struct` skip rule).  Mirror that in conversions: skip cfg-gated
            // fields and let `..Default::default()` pad the core struct slot.
            strip_cfg_fields_from_binding_struct: true,
            ..alef_codegen::conversions::ConversionConfig::default()
        };
        for typ in api.types.iter().filter(|typ| !typ.is_trait) {
            // binding→core: emit when type is used as input and all fields are
            // convertible (mirrors pyo3/magnus emission paths).
            if input_types.contains(&typ.name)
                && alef_codegen::conversions::can_generate_conversion(typ, &binding_to_core)
            {
                builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
                    typ,
                    &core_import,
                    &extendr_conversion_cfg,
                ));
            }
            // core→binding: emit whenever the conversion can be generated.  Allows
            // `core_value.into()` in return positions.
            if alef_codegen::conversions::can_generate_conversion(typ, &core_to_binding) {
                builder.add_item(&alef_codegen::conversions::gen_from_core_to_binding_cfg(
                    typ,
                    &core_import,
                    &opaque_types,
                    &extendr_conversion_cfg,
                ));
            }
        }
        for e in &api.enums {
            if is_flat_data_enum(e) {
                // Flat struct: generate dedicated From<core::Enum> impl that populates variant fields.
                if alef_codegen::conversions::can_generate_enum_conversion_from_core(e) {
                    builder.add_item(&gen_extendr_flat_data_enum_from_core(e, &core_import));
                    // Also generate the reverse for flat data enums whose tuple variant fields are
                    // all primitive/String types (so binding→core round-trip works).
                    // Output-only enums like FormatMetadata have complex output-only variant types
                    // (PdfMetadata, DocxMetadata, ...) and are excluded.
                    if can_flat_data_enum_round_trip(e) {
                        builder.add_item(&gen_extendr_flat_data_enum_to_core(e, &core_import));
                    }
                }
                // binding→core is only generated for round-trip-safe flat data enums above.
            } else if is_json_passthrough_data_enum(e) {
                // JSON-passthrough wrapper struct already emits its own `From<core>`
                // and `From<binding>` impls in `gen_extendr_json_passthrough_enum_struct`.
                // Skip the generic enum conversion which would emit a lossy unit-variant
                // mapping that conflicts with the wrapper struct definition.
                continue;
            } else {
                // Extendr emits enums as flat (unit-only) variants regardless of whether the
                // core enum has data — emit lossy From impls so containing structs can call
                // `.into()`.  Data is discarded across the boundary; the binding enum keeps
                // only the variant tag.
                if input_types.contains(&e.name) && alef_codegen::conversions::can_generate_enum_conversion(e) {
                    builder.add_item(&alef_codegen::conversions::gen_enum_from_binding_to_core(
                        e,
                        &core_import,
                    ));
                }
                if alef_codegen::conversions::can_generate_enum_conversion_from_core(e) {
                    builder.add_item(&alef_codegen::conversions::gen_enum_from_core_to_binding(
                        e,
                        &core_import,
                    ));
                }
            }
        }

        // Collect non-excluded bridges for function param matching.
        let active_bridges: Vec<_> = config
            .trait_bridges
            .iter()
            .filter(|b| !b.exclude_languages.iter().any(|l| l == "r" || l == "extendr"))
            .cloned()
            .collect();

        // Generate function bindings
        for func in &api.functions {
            let bridge_param = crate::trait_bridge::find_bridge_param(func, &active_bridges);
            let bridge_field = find_bridge_field(func, &api.types, &active_bridges);

            if let Some((param_idx, bridge_cfg)) = bridge_param {
                builder.add_item(&crate::trait_bridge::gen_bridge_function(
                    func,
                    param_idx,
                    bridge_cfg,
                    self,
                    &opaque_types,
                    &core_import,
                ));
            } else if let Some(bm) = bridge_field {
                // Function has a bridge field binding (e.g., visitor on options)
                builder.add_item(&gen_extendr_bridge_field_function(func, &bm, &core_import));
            } else {
                // Detect functions whose return type or parameter types are incompatible
                // with extendr's automatic Robj conversions. These need JSON bridging.
                let func_return_needs_json = return_type_needs_json(
                    &func.return_type,
                    &extendr_incompatible_types,
                    &enum_names,
                    &opaque_types,
                );
                let func_params_need_json = func.params.iter().any(|p| is_extendr_native_incompatible(&p.ty));
                if func_return_needs_json || func_params_need_json {
                    builder.add_item(&gen_extendr_json_bridged_function(
                        func,
                        self,
                        &core_import,
                        &opaque_types,
                        &cfg,
                        &extendr_incompatible_types,
                    ));
                } else {
                    builder.add_item(&generators::gen_function(
                        func,
                        self,
                        &cfg,
                        &adapter_bodies,
                        &opaque_types,
                    ));
                }
            }
        }

        // Trait bridge wrappers — generate extendr bridge structs that delegate to R list objects
        let mut emitted_send_robj_helper = false;
        for bridge_cfg in &config.trait_bridges {
            // Skip bridges explicitly excluded for this language.
            if bridge_cfg.exclude_languages.iter().any(|l| l == "r" || l == "extendr") {
                continue;
            }
            if let Some(trait_type) = api.types.iter().find(|t| t.is_trait && t.name == bridge_cfg.trait_name) {
                // Emit the shared SendRobj wrapper once before the first bridge struct so async
                // bridge methods can move `Robj` clones into `tokio::spawn_blocking` closures.
                if !emitted_send_robj_helper {
                    builder.add_item(crate::trait_bridge::gen_send_robj_helper());
                    emitted_send_robj_helper = true;
                }
                let bridge = crate::trait_bridge::gen_trait_bridge(
                    trait_type,
                    bridge_cfg,
                    &core_import,
                    &config.error_type_name(),
                    &config.error_constructor_expr(),
                    api,
                );
                for imp in &bridge.imports {
                    builder.add_import(imp);
                }
                builder.add_item(&bridge.code);
            }
        }

        // Module registration — only include types that were actually generated.
        // Arc-incompatible opaque types (e.g. VisitorHandle) were skipped above and
        // must be omitted from the module so the linker/R doesn't expect them.
        let module_name = config.r_package_name().replace('-', "_");
        let module_items = format!(
            "extendr_module! {{\n    mod {module};\n{types}{flat_enums}{json_enums}{funcs}}}\n",
            module = module_name,
            types = api
                .types
                .iter()
                .filter(|t| {
                    !t.is_trait
                        && !arc_incompatible_opaque.contains(&t.name)
                        && !extendr_incompatible_types.contains(&t.name)
                })
                .map(|t| format!("    impl {};\n", t.name))
                .collect::<String>(),
            flat_enums = flat_data_enum_names_vec
                .iter()
                .map(|n| format!("    impl {n};\n"))
                .collect::<String>(),
            json_enums = json_passthrough_enum_names_vec
                .iter()
                .map(|n| format!("    impl {n};\n"))
                .collect::<String>(),
            funcs = api
                .functions
                .iter()
                .map(|f| format!("    fn {};\n", f.name))
                .collect::<String>(),
        );
        builder.add_item(&module_items);

        let output_path = resolve_output_dir(config.output_paths.get("r"), &config.name, "packages/r/src/rust/src");

        Ok(vec![GeneratedFile {
            path: PathBuf::from(&output_path).join("lib.rs"),
            content: builder.build(),
            generated_header: false,
        }])
    }

    fn generate_public_api(
        &self,
        api: &ApiSurface,
        config: &ResolvedCrateConfig,
    ) -> anyhow::Result<Vec<GeneratedFile>> {
        let package_name = config.r_package_name();

        // The R wrapper file always goes into the package's R/ directory (e.g. packages/r/R/).
        // We derive this from the rust output path: strip the conventional Rust-source suffix
        // (src/rust/src) and append R/, falling back to the hardcoded default.
        let r_wrapper_dir = if let Some(rust_out) = config.output_paths.get("r") {
            let rust_str = rust_out.to_string_lossy();
            // Strip trailing separator variants of "src/rust/src"
            let suffixes = ["src/rust/src/", "src/rust/src"];
            let base = suffixes
                .iter()
                .find_map(|s| rust_str.strip_suffix(s))
                .unwrap_or_else(|| rust_str.as_ref());
            format!("{base}R/")
        } else {
            "packages/r/R/".to_string()
        };
        // The NAMESPACE / DESCRIPTION sit one directory above R/ (i.e. packages/r/).
        let r_pkg_dir = r_wrapper_dir.trim_end_matches("R/").trim_end_matches("R");

        let mut files = Vec::new();

        // {package_name}.R: only @useDynLib stub. The actual call wrappers and class
        // dispatchers live in extendr-wrappers.R which is regenerated below.
        let mut pkg_content = hash::header(CommentStyle::Hash);
        pkg_content.push('\n');
        pkg_content.push_str(&crate::template_env::render(
            "r_use_dyn_lib.jinja",
            minijinja::context! { package_name => package_name },
        ));
        pkg_content.push_str("NULL\n");
        files.push(GeneratedFile {
            path: PathBuf::from(&r_wrapper_dir).join(format!("{package_name}.R")),
            content: pkg_content,
            generated_header: false,
        });

        // extendr-wrappers.R: R-side bindings for every `#[extendr]` function and method
        // registered in the `extendr_module!` macro. Without this file, R callers cannot
        // invoke the native `wrap__<symbol>` entry points exported by the .so.
        // Historically `rextendr::document()` produced this at package-development time;
        // alef now emits it directly so install-time builds (which never run rextendr)
        // still expose the public API.
        let input_type_names = alef_codegen::conversions::input_type_names(api);
        let wrappers_content = gen_extendr_wrappers_r(api, &package_name, &input_type_names);
        files.push(GeneratedFile {
            path: PathBuf::from(&r_wrapper_dir).join("extendr-wrappers.R"),
            content: wrappers_content,
            generated_header: false,
        });

        // NAMESPACE: regenerated each run so that newly added `#[extendr]` functions and
        // methods are exported. Scaffolding only writes a useDynLib bootstrap on init.
        let namespace_content = gen_namespace(api, &package_name);
        files.push(GeneratedFile {
            path: PathBuf::from(r_pkg_dir).join("NAMESPACE"),
            content: namespace_content,
            generated_header: false,
        });

        // options.R: generated from ConversionOptions IR so all fields are present.
        if let Some(opts_type) = api.types.iter().find(|t| t.name == "ConversionOptions" && !t.is_trait) {
            let options_r = gen_conversion_options_r(opts_type);
            files.push(GeneratedFile {
                path: PathBuf::from(&r_wrapper_dir).join("options.R"),
                content: options_r,
                generated_header: true,
            });
        }

        Ok(files)
    }

    fn build_config(&self) -> Option<BuildConfig> {
        Some(BuildConfig {
            tool: "cargo",
            crate_suffix: "-extendr",
            build_dep: BuildDependency::None,
            post_build: vec![],
        })
    }
}

/// Generate the `options.R` file for the R package from the `ConversionOptions` IR type.
///
/// Produces a roxygen-documented `conversion_options()` helper function with one parameter per
/// field (all defaulting to `NULL`). R callers use named arguments to override individual
/// settings; unset parameters remain `NULL` and are omitted from the resulting list so that the
/// Rust side applies its own defaults.
fn gen_conversion_options_r(opts_type: &TypeDef) -> String {
    use alef_core::ir::PrimitiveType;

    // Build parameter list
    let params: Vec<String> = opts_type
        .fields
        .iter()
        .map(|f| format!("{} = NULL", f.name.trim_start_matches('_')))
        .collect();

    // Build field info for template
    let fields: Vec<minijinja::Value> = opts_type
        .fields
        .iter()
        .map(|field| {
            let rname = field.name.trim_start_matches('_');
            let doc_text = if field.doc.is_empty() {
                rname.to_string()
            } else {
                let first = field.doc.lines().next().unwrap_or(rname);
                first.trim_end_matches('.').to_string()
            };

            let needs_int = matches!(
                &field.ty,
                TypeRef::Primitive(PrimitiveType::U8)
                    | TypeRef::Primitive(PrimitiveType::U16)
                    | TypeRef::Primitive(PrimitiveType::U32)
                    | TypeRef::Primitive(PrimitiveType::U64)
                    | TypeRef::Primitive(PrimitiveType::I8)
                    | TypeRef::Primitive(PrimitiveType::I16)
                    | TypeRef::Primitive(PrimitiveType::I32)
                    | TypeRef::Primitive(PrimitiveType::I64)
                    | TypeRef::Primitive(PrimitiveType::Usize)
            );
            let assign_val = if needs_int {
                format!("as.integer({rname})")
            } else {
                rname.to_string()
            };

            minijinja::context! {
                rname => rname,
                doc => doc_text,
                cfg => field.cfg.is_some(),
                assign_val => assign_val,
            }
        })
        .collect();

    crate::template_env::render(
        "conversion_options.jinja",
        minijinja::context! {
            params => params,
            fields => fields,
        },
    )
}

/// Returns true if the function return type cannot be handled by extendr's `#[extendr]` macro
/// automatically, and therefore requires JSON bridging.
///
/// Extendr cannot handle:
///   - Named types in `extendr_incompatible_types` (structs with Vec<T> fields)
///   - Vec<Named> where Named is extendr-incompatible
///   - Vec<Vec<_>> (no Robj impl for nested vectors)
///   - Option<Enum> (enums don't implement ToVectorValue, so Option<Enum> fails)
///   - Option<non-opaque struct> (Option<ExternalPtr<T>> doesn't implement ToVectorValue)
///
/// Returns true if `e` is a data enum where every data-carrying variant is either a
/// single-field tuple variant or a single-field struct variant. Such enums are
/// represented as flat structs in the R binding so that callers can access variant
/// data with dot notation (e.g. `result$format$excel$sheet_count`).
///
/// Single-field struct variants like `Preset { name: String }` are treated
/// identically to single-field tuple variants `Preset(String)` — both expose one
/// scalar field per variant in the flat struct.
fn is_flat_data_enum(e: &alef_core::ir::EnumDef) -> bool {
    let has_data = e.variants.iter().any(|v| !v.fields.is_empty());
    has_data
        && e.variants
            .iter()
            .filter(|v| !v.fields.is_empty())
            .all(|v| v.fields.len() == 1)
}

/// Returns true if a flat data enum can safely generate a binding→core From impl.
/// Only enums whose tuple variant data is String or Option<String> are safe — complex
/// output-only struct types (DocxMetadata, PdfMetadata, etc.) have no reverse conversion.
fn can_flat_data_enum_round_trip(e: &alef_core::ir::EnumDef) -> bool {
    e.variants.iter().all(|v| {
        if v.fields.is_empty() {
            return true; // unit variants always safe
        }
        if v.is_tuple && v.fields.len() == 1 {
            let ty = &v.fields[0].ty;
            matches!(ty, alef_core::ir::TypeRef::String)
                || matches!(ty, alef_core::ir::TypeRef::Optional(inner) if matches!(inner.as_ref(), alef_core::ir::TypeRef::String))
        } else {
            false
        }
    })
}

/// Returns true if `e` is a tagged data enum (i.e. has `serde_tag`) that cannot be
/// represented as a flat struct, but can be safely round-tripped through serde JSON
/// — at least one variant has data and `is_flat_data_enum` returns false. These
/// enums get a JSON-passthrough binding (newtype around the core type's serde
/// JSON encoding) so the variant payload survives the FFI boundary.
///
/// The core type must implement `Serialize`/`Deserialize` consistently with the wire
/// format. Tagged enums in supported source crates derive both unconditionally, so this is safe.
fn is_json_passthrough_data_enum(e: &alef_core::ir::EnumDef) -> bool {
    if is_flat_data_enum(e) {
        return false;
    }
    if e.serde_tag.is_none() {
        return false;
    }
    e.variants.iter().any(|v| !v.fields.is_empty())
}

/// Generate a JSON-passthrough wrapper struct for a tagged data enum.
///
/// The wrapper carries the serde-JSON encoding of the core enum value in a private
/// `__inner` field. `#[serde(from, into)]` plugs the wrapper into serde so nested
/// deserialization through parent binding structs (e.g. `EmbeddingConfig::from_json`)
/// preserves the inner variant data transparently — the parent's serde derives drive
/// the bridge with no extra glue.
///
/// The struct exposes `from_json(json: String)` (for direct construction from R) and
/// `default()`. From/Into impls bridge to the core type via serde round-trip.
fn gen_extendr_json_passthrough_enum_struct(enum_def: &alef_core::ir::EnumDef, core_import: &str) -> String {
    let name = &enum_def.name;
    let core_path = format!("{core_import}::{name}");
    format!(
        r#"#[extendr]
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
#[serde(from = "{core_path}", into = "{core_path}")]
pub struct {name} {{
    /// Serde-JSON encoding of the underlying core enum value. Preserves the
    /// tagged-variant payload across the FFI boundary so round trips don't drop
    /// inner field data. The field is private-by-convention (double-underscore
    /// prefix) and not surfaced in R; construction goes through `from_json`.
    #[serde(skip)]
    pub __inner: String,
}}

impl From<{core_path}> for {name} {{
    fn from(value: {core_path}) -> Self {{
        Self {{
            __inner: serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
        }}
    }}
}}

impl From<{name}> for {core_path} {{
    fn from(value: {name}) -> Self {{
        if value.__inner.is_empty() {{
            return <{core_path}>::default();
        }}
        serde_json::from_str(&value.__inner).unwrap_or_default()
    }}
}}

#[extendr]
impl {name} {{
    #[allow(clippy::should_implement_trait)]
    pub fn default() -> {name} {{
        <{core_path}>::default().into()
    }}
    pub fn from_json(json: String) -> extendr_api::Result<{name}> {{
        let core: {core_path} =
            serde_json::from_str(&json).map_err(|e| extendr_api::Error::Other(e.to_string()))?;
        Ok(core.into())
    }}
}}
"#
    )
}

/// Generate an extendr function with bridge field binding support.
///
/// For R, the function accepts the options as an Robj (R list), extracts the bridge field
/// from it, creates the bridge, injects it into the decoded options struct, and calls the
/// core function. This is similar to PyO3's gen_bridge_field_function but tailored to R.
fn gen_extendr_bridge_field_function(
    func: &FunctionDef,
    bridge_match: &alef_codegen::generators::trait_bridge::BridgeFieldMatch<'_>,
    core_import: &str,
) -> String {
    let func_name = &func.name;
    let options_param = &bridge_match.param_name;
    let field_name = &bridge_match.field_name;

    // Build the param list for the Rust function signature.
    // Non-options params are emitted with the closest extendr-convertible Rust type
    // so the call site can hand them to the core function with the expected reference shape.
    let mut param_parts = Vec::new();
    for param in &func.params {
        if param.name == *options_param {
            // Options param is always Robj in the bridge_field case (decoded by decode_options).
            param_parts.push(format!("{}: Robj", param.name));
        } else {
            match &param.ty {
                TypeRef::String => param_parts.push(format!("{}: String", param.name)),
                _ => param_parts.push(format!("{}: Robj", param.name)),
            }
        }
    }
    let params_str = param_parts.join(", ");

    // Return type
    let return_type = "Result<Robj>";

    let mut body = String::with_capacity(1024);

    // Extract the bridge field from the options Robj (which is a list)
    body.push_str("    use std::sync::Arc;\n");
    body.push_str("    use std::sync::Mutex;\n");
    body.push_str(&format!(
        "    let {field_name}_robj: Option<Robj> = {options_param}.clone().as_list().and_then(|list| {{\n"
    ));
    body.push_str(&format!(
        "        list.iter().find(|(k, _)| *k == \"{field_name}\").map(|(_, v)| v)\n"
    ));
    body.push_str("    }).filter(|v| !v.is_null() && !v.is_na());\n");

    // Create the bridge handle from the R object
    body.push_str(&format!(
        "    let {field_name}_handle: Option<{core_import}::visitor::VisitorHandle> = {field_name}_robj\n"
    ));
    body.push_str(&format!(
        "        .map(|v| Arc::new(Mutex::new(RHtmlVisitorBridge::new(v))) as {core_import}::visitor::VisitorHandle);\n"
    ));

    // Decode options and inject the bridge
    body.push_str(&format!(
        "    let mut opts = crate::options::decode_options({options_param})\n"
    ));
    body.push_str("        .map_err(|e| extendr_api::Error::Other(e))?;\n");
    body.push_str(&format!("    opts.{field_name} = {field_name}_handle;\n"));

    // Build the core function call. For `String` params we pass `&name` (deref-coerces to `&str`);
    // for `Robj` fall back to passing by reference as before. Options always becomes `Some(opts)`.
    let mut call_args = Vec::new();
    for param in &func.params {
        if param.name == *options_param {
            call_args.push("Some(opts)".to_string());
        } else {
            call_args.push(format!("&{}", param.name));
        }
    }
    body.push_str(&format!("    {core_import}::{func_name}({})\n", call_args.join(", ")));
    body.push_str("        .map(crate::types::conversion_result_to_robj)\n");
    body.push_str("        .map_err(|e| extendr_api::Error::Other(e.to_string()))\n");

    format!("#[extendr]\npub fn {func_name}({params_str}) -> {return_type} {{\n{body}}}\n")
}

/// Generate a flat Rust struct for a data enum with all-tuple variants.
///
/// The struct has a discriminator field (from `serde_tag`, defaulting to `"format_type"`)
/// plus one `Option<T>` field per data-carrying variant. The variant field name is the
/// snake_case form of the variant name (e.g. `Excel` → `excel`).
///
/// `#[derive(Default)]` is required so `From` impls can use `..Default::default()`.
/// `serde::Serialize`/`Deserialize` are required so the JSON bridge produces and consumes
/// the nested representation.
fn gen_extendr_flat_data_enum_struct(
    enum_def: &alef_core::ir::EnumDef,
    mapper: &dyn alef_codegen::type_mapper::TypeMapper,
    cfg: &alef_codegen::generators::RustBindingConfig,
) -> String {
    let name = &enum_def.name;
    let discriminator = enum_def.serde_tag.as_deref().unwrap_or("format_type");
    let mut out = String::with_capacity(1024);

    // Build derives: start with the binding config derives (e.g. ["Clone"]), then add
    // Default (for ..Default::default() in From impls) and serde derives for JSON bridge.
    let mut derives: Vec<&str> = cfg.struct_derives.to_vec();
    derives.push("Default");
    derives.push("serde::Serialize");
    derives.push("serde::Deserialize");
    out.push_str(&crate::template_env::render(
        "flat_enum_derive.jinja",
        minijinja::context! {
            derives => derives.join(", "),
        },
    ));

    out.push_str(&crate::template_env::render(
        "flat_enum_struct_header.jinja",
        minijinja::context! {
            name => name,
        },
    ));
    out.push_str(&crate::template_env::render(
        "flat_enum_discriminator_field.jinja",
        minijinja::context! {
            discriminator => discriminator,
        },
    ));

    for variant in &enum_def.variants {
        if !variant.fields.is_empty() && variant.is_tuple {
            if let Some(first_field) = variant.fields.first() {
                let field_name = heck::AsSnakeCase(variant.name.as_str()).to_string();
                let inner_ty = mapper.map_type(&first_field.ty);
                out.push_str(&crate::template_env::render(
                    "flat_enum_variant_field.jinja",
                    minijinja::context! {
                        field_name => &field_name,
                        inner_ty => &inner_ty,
                    },
                ));
            }
        }
    }

    out.push_str(&crate::template_env::render(
        "flat_enum_struct_footer.jinja",
        minijinja::context! {},
    ));
    out
}

/// Generate a `From<core::EnumName> for FlatStruct` impl for flat data enums.
///
/// The generic `gen_enum_from_core_to_binding` generates enum→enum arm matching which does
/// not apply to flat structs. This function generates the correct struct-init form.
fn gen_extendr_flat_data_enum_from_core(enum_def: &alef_core::ir::EnumDef, core_import: &str) -> String {
    let name = &enum_def.name;
    let core_path = format!("{core_import}::{name}");
    let discriminator = enum_def.serde_tag.as_deref().unwrap_or("format_type");
    let mut out = String::with_capacity(512);

    let variant_wire_name = |variant: &alef_core::ir::EnumVariant| -> String {
        if let Some(r) = &variant.serde_rename {
            return r.clone();
        }
        match enum_def.serde_rename_all.as_deref() {
            Some("snake_case") => heck::AsSnakeCase(variant.name.as_str()).to_string(),
            Some("camelCase") => heck::AsLowerCamelCase(variant.name.as_str()).to_string(),
            Some("SCREAMING_SNAKE_CASE") => heck::AsShoutySnakeCase(variant.name.as_str()).to_string(),
            Some("kebab-case") => heck::AsKebabCase(variant.name.as_str()).to_string(),
            _ => variant.name.clone(),
        }
    };

    out.push_str(&crate::template_env::render(
        "flat_enum_from_core_impl.jinja",
        minijinja::context! {
            core_path => &core_path,
            name => name,
        },
    ));

    for variant in &enum_def.variants {
        let field_name = heck::AsSnakeCase(variant.name.as_str()).to_string();
        let wire_name = variant_wire_name(variant);
        if variant.fields.is_empty() {
            out.push_str(&crate::template_env::render(
                "flat_enum_from_core_variant_unit.jinja",
                minijinja::context! {
                    core_path => &core_path,
                    vname => &variant.name,
                    disc => discriminator,
                    wire => &wire_name,
                },
            ));
        } else if variant.is_tuple {
            let first_field = variant.fields.first().unwrap();
            let is_boxed = first_field.is_boxed;
            let is_sanitized_to_string =
                first_field.sanitized && matches!(first_field.ty, alef_core::ir::TypeRef::String);
            let data_expr: String = if is_sanitized_to_string {
                if is_boxed {
                    "format!(\"{:?}\", *_0)".to_string()
                } else {
                    "format!(\"{:?}\", _0)".to_string()
                }
            } else if is_boxed {
                "(*_0).into()".to_string()
            } else {
                "_0.into()".to_string()
            };
            out.push_str(&crate::template_env::render(
                "flat_enum_from_core_variant_tuple.jinja",
                minijinja::context! {
                    core_path => &core_path,
                    vname => &variant.name,
                    disc => discriminator,
                    wire => &wire_name,
                    fname => &field_name,
                    expr => &data_expr,
                },
            ));
        }
    }

    out.push_str(&crate::template_env::render(
        "flat_enum_from_core_impl_footer.jinja",
        minijinja::context! {},
    ));
    out
}

fn gen_extendr_flat_data_enum_to_core(enum_def: &alef_core::ir::EnumDef, core_import: &str) -> String {
    let name = &enum_def.name;
    let core_path = format!("{core_import}::{name}");
    let discriminator = enum_def.serde_tag.as_deref().unwrap_or("format_type");
    let mut out = String::with_capacity(512);

    let variant_wire_name = |variant: &alef_core::ir::EnumVariant| -> String {
        if let Some(r) = &variant.serde_rename {
            return r.clone();
        }
        match enum_def.serde_rename_all.as_deref() {
            Some("snake_case") => heck::AsSnakeCase(variant.name.as_str()).to_string(),
            Some("camelCase") => heck::AsLowerCamelCase(variant.name.as_str()).to_string(),
            Some("SCREAMING_SNAKE_CASE") => heck::AsShoutySnakeCase(variant.name.as_str()).to_string(),
            Some("kebab-case") => heck::AsKebabCase(variant.name.as_str()).to_string(),
            _ => variant.name.clone(),
        }
    };

    out.push_str(&crate::template_env::render(
        "flat_enum_from_binding_impl.jinja",
        minijinja::context! {
            name => name,
            core_path => &core_path,
            discriminator => discriminator,
        },
    ));

    for variant in &enum_def.variants {
        let field_name = heck::AsSnakeCase(variant.name.as_str()).to_string();
        let wire_name = variant_wire_name(variant);
        if variant.fields.is_empty() {
            out.push_str(&crate::template_env::render(
                "flat_enum_from_binding_variant_unit.jinja",
                minijinja::context! {
                    wire => &wire_name,
                    vname => &variant.name,
                },
            ));
        } else if variant.is_tuple {
            out.push_str(&crate::template_env::render(
                "flat_enum_from_binding_variant_tuple.jinja",
                minijinja::context! {
                    wire => &wire_name,
                    vname => &variant.name,
                    fname => &field_name,
                },
            ));
        }
    }

    out.push_str(&crate::template_env::render(
        "flat_enum_from_binding_impl_footer.jinja",
        minijinja::context! {},
    ));
    out
}

fn return_type_needs_json(
    ret: &TypeRef,
    extendr_incompatible_types: &AHashSet<String>,
    enum_names: &AHashSet<String>,
    opaque_types: &AHashSet<String>,
) -> bool {
    match ret {
        TypeRef::Named(n) => extendr_incompatible_types.contains(n.as_str()),
        TypeRef::Vec(inner) => match inner.as_ref() {
            TypeRef::Named(n) => extendr_incompatible_types.contains(n.as_str()),
            // Vec<Vec<_>> has no From<Vec<Vec<T>>> for Robj regardless of T
            TypeRef::Vec(_) => true,
            _ => false,
        },
        TypeRef::Optional(inner) => match inner.as_ref() {
            // Option<Enum>: extendr enums don't implement ToVectorValue
            TypeRef::Named(n) if enum_names.contains(n.as_str()) => true,
            // Option<non-opaque struct>: Option<ExternalPtr<T>> doesn't implement
            // ToVectorValue — bridge via JSON so R receives NULL or a JSON string.
            TypeRef::Named(n) if !opaque_types.contains(n.as_str()) && !enum_names.contains(n.as_str()) => true,
            _ => false,
        },
        _ => false,
    }
}

/// Generate a JSON-bridged `#[extendr]` free function.
///
/// When a function's return type or parameter types cannot be handled by extendr's automatic
/// Robj conversions, this generates a wrapper that:
///   - For incompatible return types (ExtractionResult, Vec<ExtractionResult>, Vec<Vec<f32/f64>>,
///     Option<Enum>): serializes the Rust result to a JSON string via serde_json.
///   - For incompatible parameter types (Vec<Struct>): takes a JSON `String` and deserializes it.
///   - Async functions use the TokioBlockOn pattern (no `async fn`).
fn gen_extendr_json_bridged_function(
    func: &FunctionDef,
    mapper: &dyn TypeMapper,
    core_import: &str,
    opaque_types: &AHashSet<String>,
    cfg: &RustBindingConfig,
    extendr_incompatible_types: &AHashSet<String>,
) -> String {
    use alef_codegen::generators::binding_helpers::gen_call_args_cfg;

    let err_map = ".map_err(|e| extendr_api::Error::Other(e.to_string().replace(\":\", \"_\").replace(\"/\", \"_\").replace(\"-\", \"_\").chars().take(255).collect::<String>()))";
    let rt_new = format!("tokio::runtime::Runtime::new(){err_map}?");

    // Build the parameter list. For Vec<Struct> params (extendr-incompatible),
    // take `String` and deserialize from JSON.
    let mut sig_params: Vec<String> = Vec::new();
    let mut body_preamble = String::new();

    for param in &func.params {
        let needs_json = matches!(&param.ty, TypeRef::Vec(inner)
            if matches!(inner.as_ref(), TypeRef::Named(n) if !opaque_types.contains(n.as_str())));
        if needs_json {
            // Take JSON string, deserialize to core Vec<T>.
            let core_ty_path = match &param.ty {
                TypeRef::Vec(inner) => match inner.as_ref() {
                    TypeRef::Named(n) => format!("{core_import}::{n}"),
                    _ => unreachable!(),
                },
                _ => unreachable!(),
            };
            if param.optional {
                sig_params.push(format!("{}: Option<String>", param.name));
                body_preamble.push_str(&crate::template_env::render(
                    "json_vec_optional_preamble.jinja",
                    minijinja::context! {
                        name => &param.name,
                        ty => &core_ty_path,
                        err_map => &err_map,
                    },
                ));
                body_preamble.push_str("    ");
            } else {
                sig_params.push(format!("{}: String", param.name));
                body_preamble.push_str(&crate::template_env::render(
                    "json_vec_required_preamble.jinja",
                    minijinja::context! {
                        name => &param.name,
                        ty => &core_ty_path,
                        err_map => &err_map,
                    },
                ));
                body_preamble.push_str("    ");
            }
        } else {
            // Use the standard binding type.
            let ty_str = mapper.map_type(&param.ty);
            // Named non-opaque structs must be `&T` (extendr TryFrom<&Robj> for &T).
            let sig_ty = if matches!(&param.ty, TypeRef::Named(n) if !opaque_types.contains(n.as_str())) {
                if param.optional {
                    format!("extendr_api::Nullable<&{ty_str}>")
                } else {
                    format!("&{ty_str}")
                }
            } else if param.optional {
                format!("Option<{ty_str}>")
            } else {
                ty_str
            };
            sig_params.push(format!("{}: {sig_ty}", param.name));
        }
    }

    // Build the call argument list. For JSON-deserialized params, use the _core variable.
    let call_args: Vec<String> = func
        .params
        .iter()
        .map(|param| {
            let needs_json = matches!(&param.ty, TypeRef::Vec(inner)
            if matches!(inner.as_ref(), TypeRef::Named(n) if !opaque_types.contains(n.as_str())));
            if needs_json {
                if param.optional {
                    format!("{}_core.as_deref().unwrap_or_default()", param.name)
                } else {
                    format!("{}_core", param.name)
                }
            } else {
                // Use gen_call_args_cfg for regular params.
                gen_call_args_cfg(
                    std::slice::from_ref(param),
                    opaque_types,
                    cfg.cast_uints_to_i32,
                    cfg.cast_large_ints_to_f64,
                )
            }
        })
        .collect();
    let call_args_str = call_args.join(", ");

    // Determine the core function path.
    let core_fn_path = {
        let path = func.rust_path.replace('-', "_");
        if path.starts_with(core_import) {
            path
        } else {
            format!("{core_import}::{}", func.name)
        }
    };

    // Handle named non-opaque struct params that need let bindings (for &T conversion).
    let mut named_let_bindings = String::new();
    for param in &func.params {
        let needs_json = matches!(&param.ty, TypeRef::Vec(inner)
            if matches!(inner.as_ref(), TypeRef::Named(n) if !opaque_types.contains(n.as_str())));
        if !needs_json {
            if let TypeRef::Named(n) = &param.ty {
                if !opaque_types.contains(n.as_str()) {
                    if param.optional {
                        // Nullable<&T>: use into_option() then map to core
                        named_let_bindings.push_str(&crate::template_env::render(
                            "named_let_optional_binding.jinja",
                            minijinja::context! {
                                name => &param.name,
                                ci => core_import,
                                n => n,
                            },
                        ));
                        named_let_bindings.push_str("    ");
                    } else {
                        named_let_bindings.push_str(&crate::template_env::render(
                            "named_let_required_binding.jinja",
                            minijinja::context! {
                                name => &param.name,
                                ci => core_import,
                                n => n,
                            },
                        ));
                        named_let_bindings.push_str("    ");
                    }
                }
            }
        }
    }

    // Adjust call_args for named let binding params.
    let final_call_args: Vec<String> = func
        .params
        .iter()
        .map(|param| {
            let needs_json = matches!(&param.ty, TypeRef::Vec(inner)
            if matches!(inner.as_ref(), TypeRef::Named(n) if !opaque_types.contains(n.as_str())));
            if needs_json {
                if param.optional {
                    format!("{}_core.as_deref().unwrap_or_default()", param.name)
                } else {
                    format!("{}_core", param.name)
                }
            } else if matches!(&param.ty, TypeRef::Named(n) if !opaque_types.contains(n.as_str())) {
                if param.optional {
                    format!("{}_core.as_ref()", param.name)
                } else {
                    format!("&{}_core", param.name)
                }
            } else {
                gen_call_args_cfg(
                    std::slice::from_ref(param),
                    opaque_types,
                    cfg.cast_uints_to_i32,
                    cfg.cast_large_ints_to_f64,
                )
            }
        })
        .collect();
    let _ = call_args_str; // replaced by final_call_args
    let final_call_args_str = final_call_args.join(", ");

    // Generate the return type — always String (JSON) for JSON-bridged functions,
    // or Option<String> for Option<Enum> returns.
    let (ret_type, result_convert) = match &func.return_type {
        TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Named(_)) => {
            // Option<Enum>: return Option<String> (or Result<Option<String>> if fallible).
            // `.transpose()` converts Option<Result<S,E>> → Result<Option<S>,E> for the
            // Result case; for the non-Result case it converts Option<Result<S,E>> → Result<Option<S>,E>
            // which we then `?`-unwrap to Option<String>.
            if func.error_type.is_some() {
                // Last expression must be Result<Option<String>, E> — use transpose() without `?`
                let ser = format!(
                    "result.map(|v| serde_json::to_string(&v){err_map}).transpose()",
                    err_map = err_map
                );
                ("Result<Option<String>>".to_string(), ser)
            } else {
                // No error type: result is Option<Enum>, serialize each Some variant
                let ser = "result.map(|v| serde_json::to_string(&v).expect(\"serialization failed\"))".to_string();
                ("Option<String>".to_string(), ser)
            }
        }
        _ => {
            // All other incompatible types: return String (JSON)
            // For Result<String> functions, the last expr must be Result<String, E> — use map_err
            // without `?` (the `?` would unwrap to String, not match Result<String>).
            if func.error_type.is_some() {
                let ser = format!("serde_json::to_string(&result){err_map}");
                ("Result<String>".to_string(), ser)
            } else {
                (
                    "String".to_string(),
                    "serde_json::to_string(&result).expect(\"serialization failed\")".to_string(),
                )
            }
        }
    };

    // Determine if the core result must be converted to a binding type before serialization.
    // Named types in extendr_incompatible_types have full core→binding From impls (e.g.
    // ExtractionResult). Serializing the binding type instead of the core type ensures the
    // JSON uses the flat-struct representation for data enums (e.g. FormatMetadata).
    let binding_conversion: Option<String> = match &func.return_type {
        TypeRef::Named(n) if extendr_incompatible_types.contains(n.as_str()) => {
            Some(format!("let result: {n} = result.into();"))
        }
        TypeRef::Vec(inner) => match inner.as_ref() {
            TypeRef::Named(n) if extendr_incompatible_types.contains(n.as_str()) => Some(format!(
                "let result: Vec<{n}> = result.into_iter().map(Into::into).collect();"
            )),
            _ => None,
        },
        _ => None,
    };
    let convert = binding_conversion.as_deref().unwrap_or("");

    // Build the function body.
    let core_call = format!("{core_fn_path}({final_call_args_str})");

    let body = if func.is_async {
        // Async: use TokioBlockOn (no async fn for extendr)
        if func.error_type.is_some() {
            format!(
                "{body_preamble}{named_let_bindings}\
                 let rt = {rt_new};\n    \
                 let result = rt.block_on(async {{ {core_call}.await{err_map} }})?;\n    \
                 {convert}\n    \
                 {result_convert}",
                err_map = err_map,
                result_convert = result_convert,
            )
        } else {
            format!(
                "{body_preamble}{named_let_bindings}\
                 let rt = {rt_new};\n    \
                 let result = rt.block_on(async {{ {core_call}.await }});\n    \
                 {convert}\n    \
                 {result_convert}"
            )
        }
    } else if func.error_type.is_some() {
        // Sync with error
        match &func.return_type {
            TypeRef::Optional(_) => {
                // For Option<T> returns: call core, serialize result
                format!(
                    "{body_preamble}{named_let_bindings}\
                     let result = {core_call}{err_map}?;\n    \
                     {convert}\n    \
                     {result_convert}"
                )
            }
            _ => {
                format!(
                    "{body_preamble}{named_let_bindings}\
                     let result = {core_call}{err_map}?;\n    \
                     {convert}\n    \
                     {result_convert}"
                )
            }
        }
    } else {
        // Sync without error
        format!(
            "{body_preamble}{named_let_bindings}\
             let result = {core_call};\n    \
             {convert}\n    \
             {result_convert}"
        )
    };

    // Assemble the full function.
    let params_str = sig_params.join(", ");
    let allow = if func.error_type.is_some() {
        "#[allow(clippy::missing_errors_doc)]\n"
    } else {
        ""
    };
    format!(
        "{allow}#[extendr]\npub fn {}({params_str}) -> {ret_type} {{\n    {body}\n}}",
        func.name
    )
}

/// Return the set of type names that are excluded from extendr class registration.
///
/// Mirrors the filters applied in `generate_bindings`:
///   • Trait types — never registered (no concrete class).
///   • Arc-incompatible opaque types (Rc-based, cfg-feature-gated) — skipped.
///   • Extendr-incompatible types: structs whose fields contain `Vec<T>` where T is a
///     non-opaque, non-enum named type. Extendr cannot convert these from R lists.
///
/// The returned set is used by wrapper-file generation to skip class env emission for
/// types that are not present in `extendr_module!`.
fn collect_excluded_class_types(api: &ApiSurface) -> ahash::AHashSet<String> {
    let opaque_types: ahash::AHashSet<String> = api
        .types
        .iter()
        .filter(|t| t.is_opaque)
        .map(|t| t.name.clone())
        .collect();
    let enum_names: ahash::AHashSet<String> = api.enums.iter().map(|e| e.name.clone()).collect();
    let arc_incompatible: ahash::AHashSet<String> = api
        .types
        .iter()
        .filter(|t| t.is_opaque && t.cfg.is_some())
        .map(|t| t.name.clone())
        .collect();

    let is_struct_like =
        |n: &str| -> bool { !opaque_types.contains(n) && !enum_names.contains(n) && !arc_incompatible.contains(n) };
    let is_native_incompatible = |ty: &TypeRef| -> bool {
        match ty {
            TypeRef::Vec(inner) => match inner.as_ref() {
                TypeRef::Named(n) if is_struct_like(n) => true,
                TypeRef::Vec(_) => true, // Vec<Vec<_>> not supported
                _ => false,
            },
            TypeRef::Optional(inner) => match inner.as_ref() {
                TypeRef::Vec(inner2) => match inner2.as_ref() {
                    TypeRef::Named(n) if is_struct_like(n) => true,
                    TypeRef::Vec(_) => true, // Option<Vec<Vec<_>>> not supported
                    _ => false,
                },
                _ => false,
            },
            _ => false,
        }
    };

    let mut excluded: ahash::AHashSet<String> = api
        .types
        .iter()
        .filter(|t| t.is_trait)
        .map(|t| t.name.clone())
        .collect();
    for t in &arc_incompatible {
        excluded.insert(t.clone());
    }
    for t in &api.types {
        if t.is_opaque || t.is_trait {
            continue;
        }
        if t.fields.iter().any(|f| is_native_incompatible(&f.ty)) {
            excluded.insert(t.name.clone());
        }
    }
    excluded
}

/// Return true if the method should be filtered out of an emitted impl block.
///
/// Mirrors `method_references_arc_incompatible` and `method_references_enum` from
/// `generate_bindings`. Used by wrapper-file generation to skip wrapper entries for
/// methods that the Rust impl block will not contain.
fn method_is_excluded_from_impl(method: &alef_core::ir::MethodDef, api: &ApiSurface) -> bool {
    let opaque_types: ahash::AHashSet<String> = api
        .types
        .iter()
        .filter(|t| t.is_opaque)
        .map(|t| t.name.clone())
        .collect();
    let enum_names: ahash::AHashSet<String> = api.enums.iter().map(|e| e.name.clone()).collect();
    let arc_incompatible: ahash::AHashSet<String> = api
        .types
        .iter()
        .filter(|t| t.is_opaque && t.cfg.is_some())
        .map(|t| t.name.clone())
        .collect();

    let references_arc_incompatible = |ty: &TypeRef| -> bool {
        match ty {
            TypeRef::Named(n) => arc_incompatible.contains(n),
            TypeRef::Optional(inner) => matches!(inner.as_ref(), TypeRef::Named(n) if arc_incompatible.contains(n)),
            _ => false,
        }
    };
    let references_enum = |ty: &TypeRef| -> bool {
        match ty {
            TypeRef::Named(n) => enum_names.contains(n.as_str()),
            TypeRef::Optional(inner) => matches!(inner.as_ref(), TypeRef::Named(n) if enum_names.contains(n.as_str())),
            _ => false,
        }
    };
    let param_is_owned_struct = |ty: &TypeRef| -> bool {
        let is_non_opaque_struct =
            |n: &str| !opaque_types.contains(n) && !enum_names.contains(n) && !arc_incompatible.contains(n);
        match ty {
            TypeRef::Named(n) => is_non_opaque_struct(n),
            TypeRef::Optional(inner) => matches!(inner.as_ref(), TypeRef::Named(n) if is_non_opaque_struct(n)),
            _ => false,
        }
    };

    if references_arc_incompatible(&method.return_type)
        || method.params.iter().any(|p| references_arc_incompatible(&p.ty))
    {
        return true;
    }
    if references_enum(&method.return_type)
        || method
            .params
            .iter()
            .any(|p| references_enum(&p.ty) || param_is_owned_struct(&p.ty))
    {
        return true;
    }
    if method.sanitized {
        return true;
    }
    false
}

/// Generate `extendr-wrappers.R` — the R-side bindings for every `#[extendr]` symbol
/// registered in the generated `extendr_module!` macro.
///
/// The output mirrors what `rextendr::document()` would produce at package-development
/// time, but is written directly from the alef IR so it is always present at install time.
///
/// Layout:
///   1. Free-function wrappers: `name <- function(...) .Call("wrap__name", ..., PACKAGE = "<pkg>")`.
///      Exported via `#' @export` (paired with explicit `export(name)` lines in NAMESPACE).
///   2. One `<TypeName> <- new.env(parent = emptyenv())` block per registered class, with:
///      • static methods bound as `Type$method <- function(...) .Call("wrap__Type__method", ...)`,
///      • instance methods bound as `Type$method <- function(...) .Call("wrap__Type__method", self, ...)`,
///      • dispatch operators (`$.Type`, `[[.Type`) so callers can write `instance$method(...)`.
fn gen_extendr_wrappers_r(api: &ApiSurface, package_name: &str, input_type_names: &ahash::AHashSet<String>) -> String {
    let mut out = String::with_capacity(8 * 1024);
    out.push_str("# Generated by extendr: Do not edit by hand\n");
    out.push_str("#\n");
    out.push_str("# This file is regenerated by alef on every `alef generate` run.\n");
    out.push_str("# It mirrors the output of `rextendr::document()` and binds every\n");
    out.push_str("# wrap__<symbol> entry registered in extendr_module! to an R-callable\n");
    out.push_str("# function or class env.\n\n");

    out.push_str(&crate::template_env::render(
        "r_use_dyn_lib.jinja",
        minijinja::context! { package_name => package_name },
    ));
    out.push_str("NULL\n\n");

    // Free functions. Every entry in `api.functions` is registered in extendr_module!.
    for func in &api.functions {
        let params: Vec<&str> = func.params.iter().map(|p| p.name.as_str()).collect();
        let params_sig = params.join(", ");
        let mut call_args = vec![format!("\"wrap__{}\"", func.name)];
        for p in &params {
            call_args.push((*p).to_string());
        }
        call_args.push(format!("PACKAGE = \"{package_name}\""));
        let call_args_str = call_args.join(", ");

        out.push_str(&crate::template_env::render(
            "r_free_function_wrapper.jinja",
            minijinja::context! {
                func_name => &func.name,
                params_sig => params_sig,
                call_args_str => call_args_str,
            },
        ));
    }

    // Class env blocks. One per non-trait, non-extendr-incompatible type — matching the
    // set registered in `extendr_module! { impl Type; ... }`.
    let excluded = collect_excluded_class_types(api);
    for typ in &api.types {
        if typ.is_trait || excluded.contains(&typ.name) {
            continue;
        }

        out.push_str(&crate::template_env::render(
            "r_type_class_env.jinja",
            minijinja::context! { type_name => &typ.name },
        ));

        // Emit method bindings. Skip methods that are filtered out of the Rust impl
        // block — they have no `wrap__Type__method` symbol.
        for method in &typ.methods {
            if method_is_excluded_from_impl(method, api) {
                continue;
            }
            let params: Vec<&str> = method.params.iter().map(|p| p.name.as_str()).collect();
            let params_sig = params.join(", ");
            let mut call_args = vec![format!(
                "\"wrap__{type_name}__{method_name}\"",
                type_name = typ.name,
                method_name = method.name,
            )];
            // Instance methods: extendr's wrap__Type__method symbol takes self as the
            // first argument. The class dispatch operator (`$.Type`) below captures
            // `self` from the calling environment so callers write `instance$method(...)`.
            if !method.is_static {
                call_args.push("self".to_string());
            }
            for p in &params {
                call_args.push((*p).to_string());
            }
            call_args.push(format!("PACKAGE = \"{package_name}\""));
            let call_args_str = call_args.join(", ");

            out.push_str(&crate::template_env::render(
                "r_method_binding.jinja",
                minijinja::context! {
                    type_name => &typ.name,
                    method_name => &method.name,
                    params_sig => params_sig,
                    call_args_str => call_args_str,
                },
            ));
        }

        // Synthetic from_json static factory: generated for every has_default non-opaque struct.
        // The Rust impl block adds `pub fn from_json(json: String) -> Result<Self>` which extendr
        // registers as `wrap__TypeName__from_json`. We emit the R wrapper here since from_json is
        // not part of the IR and gen_extendr_wrappers_r would otherwise skip it.
        if typ.has_default && !typ.fields.is_empty() && input_type_names.contains(&typ.name) {
            out.push_str(&crate::template_env::render(
                "r_from_json_factory.jinja",
                minijinja::context! {
                    type_name => &typ.name,
                    package_name => package_name,
                },
            ));
        }

        // Dispatch operators: `instance$method` and `instance[["method"]]` resolve via
        // the class env. The dispatcher captures `self` so instance methods see it.
        out.push_str(&crate::template_env::render(
            "r_dollar_dispatch.jinja",
            minijinja::context! { type_name => &typ.name },
        ));
        out.push_str(&crate::template_env::render(
            "r_bracket_dispatch.jinja",
            minijinja::context! { type_name => &typ.name },
        ));
    }

    // Flat data enum class env blocks — data enums are registered as structs in
    // extendr_module! and therefore need the same dispatch operator setup so R can
    // access fields via `instance$field_name`.
    for e in &api.enums {
        if !is_flat_data_enum(e) {
            continue;
        }
        let type_name = &e.name;
        out.push_str(&crate::template_env::render(
            "r_type_class_env.jinja",
            minijinja::context! { type_name => type_name },
        ));
        out.push_str(&crate::template_env::render(
            "r_dollar_dispatch.jinja",
            minijinja::context! { type_name => type_name },
        ));
        out.push_str(&crate::template_env::render(
            "r_bracket_dispatch.jinja",
            minijinja::context! { type_name => type_name },
        ));
    }

    // JSON-passthrough data enum class env blocks — these enums are also
    // registered as structs in extendr_module! with `default` and `from_json`
    // static methods. Emit the class env + method bindings + dispatchers so R
    // callers can write `EnumType$from_json("...")`.
    for e in &api.enums {
        if !is_json_passthrough_data_enum(e) {
            continue;
        }
        let type_name = &e.name;
        out.push_str(&crate::template_env::render(
            "r_type_class_env.jinja",
            minijinja::context! { type_name => type_name },
        ));
        // `default` and `from_json` are emitted by `gen_extendr_json_passthrough_enum_struct`
        // as `pub fn` items in the `#[extendr] impl` block, so extendr registers them as
        // `wrap__<EnumType>__default` and `wrap__<EnumType>__from_json`. Bind them in R
        // by name so callers can use `EnumType$default()` / `EnumType$from_json(json)`.
        for method_name in ["default", "from_json"] {
            let params_sig = if method_name == "from_json" { "json" } else { "" };
            let mut call_args = vec![format!("\"wrap__{type_name}__{method_name}\"")];
            if method_name == "from_json" {
                call_args.push("json".to_string());
            }
            call_args.push(format!("PACKAGE = \"{package_name}\""));
            let call_args_str = call_args.join(", ");
            out.push_str(&crate::template_env::render(
                "r_method_binding.jinja",
                minijinja::context! {
                    type_name => type_name,
                    method_name => method_name,
                    params_sig => params_sig,
                    call_args_str => call_args_str,
                },
            ));
        }
        out.push_str(&crate::template_env::render(
            "r_dollar_dispatch.jinja",
            minijinja::context! { type_name => type_name },
        ));
        out.push_str(&crate::template_env::render(
            "r_bracket_dispatch.jinja",
            minijinja::context! { type_name => type_name },
        ));
    }

    out
}

/// Generate `NAMESPACE` from the alef IR.
///
/// Lists every free function and every class dispatch operator (`$.Type`, `[[.Type`)
/// emitted by `gen_extendr_wrappers_r`. Without explicit `export()` entries, R loads
/// the wrapper file but treats the symbols as internal — calling code receives
/// `could not find function`.
fn gen_namespace(api: &ApiSurface, package_name: &str) -> String {
    let mut out = String::with_capacity(2 * 1024);
    out.push_str("# Generated by alef — do not edit.\n\n");
    // NAMESPACE requires the bare `useDynLib(...)` directive. The roxygen2 form
    // (`#' @useDynLib ...`) only takes effect when present in `.R` source files
    // processed by roxygen2 — emitting it directly into NAMESPACE leaves the
    // shared library unloaded and every `.Call` site fails at runtime.
    out.push_str(&crate::template_env::render(
        "r_namespace_use_dyn_lib.jinja",
        minijinja::context! { package_name => package_name },
    ));
    out.push('\n');

    for func in &api.functions {
        out.push_str(&crate::template_env::render(
            "r_namespace_export.jinja",
            minijinja::context! { name => &func.name },
        ));
    }

    let excluded = collect_excluded_class_types(api);
    for typ in &api.types {
        if typ.is_trait || excluded.contains(&typ.name) {
            continue;
        }
        out.push_str(&crate::template_env::render(
            "r_namespace_export.jinja",
            minijinja::context! { name => &typ.name },
        ));
        out.push_str(&crate::template_env::render(
            "r_namespace_s3method.jinja",
            minijinja::context! { method_type => "$", name => &typ.name },
        ));
        out.push_str(&crate::template_env::render(
            "r_namespace_s3method.jinja",
            minijinja::context! { method_type => "[[", name => &typ.name },
        ));
    }

    // Flat data enums are registered as classes in extendr_module! and need exports too.
    for e in &api.enums {
        if !is_flat_data_enum(e) {
            continue;
        }
        out.push_str(&crate::template_env::render(
            "r_namespace_export.jinja",
            minijinja::context! { name => &e.name },
        ));
        out.push_str(&crate::template_env::render(
            "r_namespace_s3method.jinja",
            minijinja::context! { method_type => "$", name => &e.name },
        ));
        out.push_str(&crate::template_env::render(
            "r_namespace_s3method.jinja",
            minijinja::context! { method_type => "[[", name => &e.name },
        ));
    }

    // JSON-passthrough data enums also need NAMESPACE exports for the class
    // env and the `$`/`[[` dispatch operators.
    for e in &api.enums {
        if !is_json_passthrough_data_enum(e) {
            continue;
        }
        out.push_str(&crate::template_env::render(
            "r_namespace_export.jinja",
            minijinja::context! { name => &e.name },
        ));
        out.push_str(&crate::template_env::render(
            "r_namespace_s3method.jinja",
            minijinja::context! { method_type => "$", name => &e.name },
        ));
        out.push_str(&crate::template_env::render(
            "r_namespace_s3method.jinja",
            minijinja::context! { method_type => "[[", name => &e.name },
        ));
    }

    out
}

#[cfg(test)]
mod tests {
    use super::ExtendrBackend;
    use alef_core::backend::Backend;
    use alef_core::config::ResolvedCrateConfig;
    use alef_core::config::new_config::NewAlefConfig;
    use alef_core::ir::*;

    fn resolved_one(toml: &str) -> ResolvedCrateConfig {
        let cfg: NewAlefConfig = toml::from_str(toml).unwrap();
        cfg.resolve().unwrap().remove(0)
    }

    fn make_config() -> ResolvedCrateConfig {
        resolved_one(
            r#"
[workspace]
languages = ["r"]

[[crates]]
name = "test-lib"
sources = ["src/lib.rs"]

[crates.r]
package_name = "testlib"
"#,
        )
    }

    fn make_field(name: &str, ty: TypeRef, optional: bool) -> FieldDef {
        FieldDef {
            name: name.to_string(),
            ty,
            optional,
            default: None,
            doc: String::new(),
            sanitized: false,
            is_boxed: false,
            type_rust_path: None,
            cfg: None,
            typed_default: None,
            core_wrapper: CoreWrapper::None,
            vec_inner_core_wrapper: CoreWrapper::None,
            newtype_wrapper: None,
            serde_rename: None,
            serde_flatten: false,
            binding_excluded: false,
            binding_exclusion_reason: None,
        }
    }

    fn make_api_surface() -> ApiSurface {
        ApiSurface {
            crate_name: "test_lib".to_string(),
            version: "0.1.0".to_string(),
            types: vec![TypeDef {
                name: "Config".to_string(),
                rust_path: "test_lib::Config".to_string(),
                original_rust_path: String::new(),
                fields: vec![make_field("timeout", TypeRef::Primitive(PrimitiveType::U32), false)],
                methods: vec![],
                is_opaque: false,
                is_clone: true,
                is_copy: false,
                is_trait: false,
                has_default: false,
                has_stripped_cfg_fields: false,
                is_return_type: false,
                serde_rename_all: None,
                has_serde: false,
                super_traits: vec![],
                doc: String::new(),
                cfg: None,
                binding_excluded: false,
                binding_exclusion_reason: None,
            }],
            functions: vec![FunctionDef {
                name: "process".to_string(),
                rust_path: "test_lib::process".to_string(),
                original_rust_path: String::new(),
                params: vec![],
                return_type: TypeRef::String,
                is_async: false,
                error_type: None,
                doc: String::new(),
                cfg: None,
                sanitized: false,
                return_sanitized: false,
                returns_ref: false,
                returns_cow: false,
                return_newtype_wrapper: None,
                binding_excluded: false,
                binding_exclusion_reason: None,
            }],
            enums: vec![],
            errors: vec![],
            excluded_type_paths: ::std::collections::HashMap::new(),
        }
    }

    #[test]
    fn generates_extendr_module_registration() {
        let backend = ExtendrBackend;
        let config = make_config();
        let api = make_api_surface();
        let files = backend.generate_bindings(&api, &config).unwrap();
        assert_eq!(files.len(), 1);
        let content = &files[0].content;
        assert!(content.contains("extendr_module!"), "must emit extendr_module! macro");
        assert!(content.contains("mod testlib"), "module name must match r_package_name");
    }

    #[test]
    fn generates_extendr_function_attribute() {
        let backend = ExtendrBackend;
        let config = make_config();
        let api = make_api_surface();
        let files = backend.generate_bindings(&api, &config).unwrap();
        let content = &files[0].content;
        assert!(
            content.contains("#[extendr]"),
            "functions must carry #[extendr] attribute"
        );
        assert!(content.contains("fn process"), "process function must be generated");
    }

    #[test]
    fn r_package_name_drives_output_path() {
        let backend = ExtendrBackend;
        let config = make_config();
        let api = make_api_surface();
        let files = backend.generate_bindings(&api, &config).unwrap();
        // Output should go to packages/r/src/rust/src/lib.rs (default path)
        assert!(
            files[0].path.to_string_lossy().ends_with("lib.rs"),
            "output file must be lib.rs"
        );
    }

    #[test]
    fn generate_public_api_uses_r_package_name() {
        let backend = ExtendrBackend;
        let config = make_config();
        let api = make_api_surface();
        let files = backend.generate_public_api(&api, &config).unwrap();
        // Expect: <package>.R (useDynLib stub), extendr-wrappers.R, NAMESPACE.
        let paths: Vec<String> = files.iter().map(|f| f.path.to_string_lossy().into_owned()).collect();
        assert!(
            paths.iter().any(|p| p.ends_with("testlib.R")),
            "public API file must include {{package_name}}.R, got {paths:?}"
        );
        assert!(
            paths.iter().any(|p| p.ends_with("extendr-wrappers.R")),
            "public API file must include extendr-wrappers.R, got {paths:?}"
        );
        assert!(
            paths.iter().any(|p| p.ends_with("NAMESPACE")),
            "public API file must include NAMESPACE, got {paths:?}"
        );
    }

    #[test]
    fn extendr_wrappers_emits_function_call_binding() {
        let backend = ExtendrBackend;
        let config = make_config();
        let api = make_api_surface();
        let files = backend.generate_public_api(&api, &config).unwrap();
        let wrappers = files
            .iter()
            .find(|f| f.path.to_string_lossy().ends_with("extendr-wrappers.R"))
            .expect("extendr-wrappers.R must be generated");
        assert!(
            wrappers.content.contains("process <- function()"),
            "free function must produce a wrapper: {}",
            wrappers.content
        );
        assert!(
            wrappers.content.contains(".Call(\"wrap__process\""),
            "wrapper must invoke the wrap__ symbol: {}",
            wrappers.content
        );
        assert!(
            wrappers.content.contains("Config <- new.env(parent = emptyenv())"),
            "non-trait class must be registered as an env: {}",
            wrappers.content
        );
    }

    #[test]
    fn namespace_exports_functions_and_classes() {
        let backend = ExtendrBackend;
        let config = make_config();
        let api = make_api_surface();
        let files = backend.generate_public_api(&api, &config).unwrap();
        let namespace = files
            .iter()
            .find(|f| f.path.to_string_lossy().ends_with("NAMESPACE"))
            .expect("NAMESPACE must be generated");
        assert!(
            namespace.content.contains("export(process)"),
            "free function must be exported: {}",
            namespace.content
        );
        assert!(
            namespace.content.contains("export(Config)"),
            "class env must be exported: {}",
            namespace.content
        );
        assert!(
            namespace.content.contains("S3method(\"$\", Config)"),
            "S3 dispatch operator must be registered: {}",
            namespace.content
        );
        // NAMESPACE must use the bare `useDynLib(...)` directive — the roxygen2
        // form (`#' @useDynLib ...`) is silently ignored by R when placed in
        // NAMESPACE, leaving the .so unloaded and every `.Call` unresolved.
        assert!(
            namespace.content.contains("useDynLib(testlib, .registration = TRUE)"),
            "NAMESPACE must contain bare useDynLib directive: {}",
            namespace.content
        );
        assert!(
            !namespace.content.contains("#' @useDynLib"),
            "NAMESPACE must not contain roxygen2 useDynLib form: {}",
            namespace.content
        );
    }
}
