//! NAPI-RS (Node.js) backend: orchestration and `Backend` trait implementation.

pub mod capsule;
mod config_opaque;
mod constructors;
pub mod enums;
pub mod errors;
pub mod functions;
pub mod methods;
mod public_api;
pub mod service_api;
mod support;
mod type_stubs;
pub mod types;

#[cfg(test)]
mod tests;

use crate::backends::napi::type_map::NapiMapper;
use crate::codegen::builder::RustFileBuilder;
use crate::codegen::generators::{self, AsyncPattern, RustBindingConfig};
use crate::core::backend::{Backend, BuildConfig, BuildDependency, Capabilities, GeneratedFile, PostBuildStep};
use crate::core::config::{Language, NodeCapsuleTypeConfig, ResolvedCrateConfig, resolve_output_dir};
use crate::core::ir::{ApiSurface, TypeRef};
use ahash::AHashSet;
use constructors::{napi_default_constructor, napi_variant_wrapper_constructor};
use std::collections::HashMap;
use std::path::PathBuf;

pub struct NapiBackend;

impl NapiBackend {
    fn binding_config<'a>(core_import: &'a str, prefix: &'a str, has_serde: bool) -> RustBindingConfig<'a> {
        RustBindingConfig {
            struct_attrs: &["napi"],
            field_attrs: &[],
            struct_derives: &["Clone"],
            method_block_attr: Some("napi"),
            constructor_attr: "#[napi(constructor)]",
            static_attr: None,
            function_attr: "#[napi]",
            enum_attrs: &["napi(string_enum)"],
            enum_derives: &["Clone"],
            needs_signature: false,
            signature_prefix: "",
            signature_suffix: "",
            core_import,
            async_pattern: AsyncPattern::NapiNativeAsync,
            has_serde,
            // NAPI napi(object) structs don't derive Serialize — disable serde bridge
            type_name_prefix: prefix,
            option_duration_on_defaults: true,
            opaque_type_names: &[],
            skip_impl_constructor: false,
            cast_uints_to_i32: false,
            cast_large_ints_to_f64: false,
            named_non_opaque_params_by_ref: false,
            lossy_skip_types: &[],
            serializable_opaque_type_names: &[],
            never_skip_cfg_field_names: &[],
            // NAPI bypasses the shared `gen_struct` and uses its own builder in
            // `src/backends/napi/gen_bindings/types.rs`, so this flag is a
            // no-op for NAPI — the napi-specific `From<JsT> for core::T` impl
            // (in `binding_to_core.rs`) uses `unwrap_or_default()` to fill
            // missing optional fields, which still emits primitive defaults.
            // Keep the flag off until the napi gen_struct learns the same
            // suppress-derive + delegate-impl pattern.
            emit_delegating_default_impl: false,
            skip_methods_when_not_delegatable: false,
        }
    }
}

impl Backend for NapiBackend {
    fn name(&self) -> &str {
        "napi"
    }

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

    fn capabilities(&self) -> Capabilities {
        Capabilities {
            supports_async: true,
            supports_classes: true,
            supports_enums: true,
            supports_option: true,
            supports_result: true,
            supports_service_api: true,
            ..Capabilities::default()
        }
    }

    fn generate_bindings(&self, api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
        let prefix = config.node_type_prefix();
        let trait_type_names: AHashSet<String> = api
            .types
            .iter()
            .filter(|t| t.is_trait)
            .map(|t| t.name.clone())
            .collect();
        let capsule_type_names_for_mapper: AHashSet<String> = config
            .node
            .as_ref()
            .map(|c| c.capsule_types.keys().cloned().collect())
            .unwrap_or_default();
        let mapper =
            NapiMapper::with_traits_and_capsules(prefix.clone(), trait_type_names, capsule_type_names_for_mapper);
        let core_import = config.core_import_name();

        // Detect serde availability from the output crate's Cargo.toml
        let output_dir = resolve_output_dir(config.output_paths.get("node"), &config.name, "crates/{name}-node/src/");
        let has_serde = crate::core::config::detect_serde_available(&output_dir);
        let mut cfg = Self::binding_config(&core_import, &prefix, has_serde);
        let never_skip_cfg_field_names: Vec<String> = config
            .trait_bridges
            .iter()
            .filter_map(|b| {
                if b.bind_via == crate::core::config::BridgeBinding::OptionsField {
                    b.resolved_options_field().map(|s| s.to_string())
                } else {
                    None
                }
            })
            .collect();
        cfg.never_skip_cfg_field_names = &never_skip_cfg_field_names;

        let mut builder = RustFileBuilder::new().with_generated_header();
        builder.add_inner_attribute("allow(dead_code, unused_imports, unused_variables)");
        builder.add_inner_attribute("allow(unsafe_code)");
        // The napi-rs crate is entirely generated glue — rustdoc coverage is not meaningful here.
        builder.add_inner_attribute("allow(missing_docs)");
        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::unnecessary_cast, clippy::unused_unit, clippy::unwrap_or_default, clippy::derivable_impls, clippy::needless_borrows_for_generic_args, clippy::unnecessary_fallible_conversions, clippy::arc_with_non_send_sync, clippy::collapsible_if, clippy::clone_on_copy, clippy::should_implement_trait)");
        // Cast lints fire heavily on the JS u32/i64/Number bridge — these are
        // intentional, deliberate at the FFI boundary. Pedantic/nursery noise
        // (must_use_candidate, use_self, missing_const_for_fn, etc.) is
        // suppressed for the same reasons documented in the pyo3 backend.
        builder.add_inner_attribute(
            "allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::default_trait_access, clippy::useless_conversion, clippy::unsafe_derive_deserialize, clippy::must_use_candidate, clippy::return_self_not_must_use, clippy::use_self, clippy::missing_const_for_fn, clippy::missing_errors_doc, clippy::needless_pass_by_value, clippy::doc_markdown, clippy::derive_partial_eq_without_eq, clippy::uninlined_format_args, clippy::redundant_clone, clippy::implicit_clone, clippy::redundant_closure_for_method_calls, clippy::wildcard_imports, clippy::option_if_let_else, clippy::too_many_lines)",
        );
        builder.add_import("napi::*");
        builder.add_import("napi_derive::napi");

        // Always import serde_json for type conversion in From/Into impls,
        // even if the binding crate doesn't explicitly list it as a dependency.
        // serde_json is needed for conversions of types with serde-serializable fields.
        builder.add_import("serde_json");

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

        // Only import HashMap when Map-typed fields or returns are present
        let has_maps = api
            .types
            .iter()
            .any(|t| t.fields.iter().any(|f| matches!(&f.ty, TypeRef::Map(_, _))))
            || api
                .functions
                .iter()
                .any(|f| matches!(&f.return_type, TypeRef::Map(_, _)));
        if has_maps {
            builder.add_import("std::collections::HashMap");
        }

        // Check if any function or method is async
        let has_async =
            api.functions.iter().any(|f| f.is_async) || api.types.iter().any(|t| t.methods.iter().any(|m| m.is_async));

        if has_async {
            builder.add_item(&functions::gen_tokio_runtime());
        }

        // Extract capsule_types from NodeConfig. Types listed here skip #[napi] opaque-class
        // emission; functions returning them produce a JsObject with __parser External<T>.
        let capsule_types: HashMap<String, NodeCapsuleTypeConfig> = config
            .node
            .as_ref()
            .map(|c| c.capsule_types.clone())
            .unwrap_or_default();

        // When capsule types are present, generated shims call set_named_property which
        // requires the JsObjectValue trait to be in scope.
        if !capsule_types.is_empty() {
            builder.add_import("napi::bindgen_prelude::JsObjectValue");
            // Emit the FFI declarations for napi_create_external and napi_type_tag_object,
            // and any per-capsule type tag constants. Done once per crate.
            builder.add_item(&capsule::gen_ffi_declarations());
            let constants = capsule::gen_type_tag_constants(&capsule_types);
            if !constants.is_empty() {
                builder.add_item(&constants);
            }
        }

        let opaque_types = config_opaque::collect_opaque_types(api, config, &capsule_types);
        let mutex_types: AHashSet<String> = api
            .types
            .iter()
            .filter(|t| t.is_opaque && generators::type_needs_mutex(t))
            .map(|t| t.name.clone())
            .collect();
        let has_traits = api.types.iter().any(|t| t.is_trait);
        if !opaque_types.is_empty() || has_traits {
            builder.add_import("std::sync::Arc");
        }
        if !mutex_types.is_empty() {
            builder.add_import("std::sync::Mutex");
        }

        let mut exclude_types: ahash::AHashSet<String> = config
            .node
            .as_ref()
            .map(|c| c.exclude_types.iter().cloned().collect())
            .unwrap_or_default();
        config_opaque::exclude_capsule_opaque_types(&mut exclude_types, config, &capsule_types);

        // Build adapter body map before type iteration so bodies are available for method generation.
        let adapter_bodies = crate::adapters::build_adapter_bodies(config, Language::Node)?;

        // Map "OwnerType.method" -> streaming item type. The napi backend needs to
        // override the IR-declared `String` return type with `Vec<{prefix}{item}>`
        // for streaming adapters, since the generated body returns chunks directly
        // as a JS array instead of a serialized JSON string.
        let streaming_item_types: ahash::AHashMap<String, String> = config
            .adapters
            .iter()
            .filter(|a| matches!(a.pattern, crate::core::config::AdapterPattern::Streaming))
            .filter_map(|a| {
                let owner = a.owner_type.as_deref()?;
                let item = a.item_type.as_deref()?;
                Some((format!("{owner}.{}", a.name), item.to_string()))
            })
            .collect();

        builder.add_item(support::js_bytes_def());

        if has_traits {
            builder.add_item(support::js_visitor_ref_def());
        }

        // Emit adapter-generated standalone items (streaming iterators, callback bridges).
        for adapter in &config.adapters {
            match adapter.pattern {
                crate::core::config::AdapterPattern::Streaming => {
                    let key = crate::adapters::stream_struct_key(adapter);
                    if let Some(struct_code) = adapter_bodies.get(&key) {
                        builder.add_item(struct_code);
                    }
                }
                crate::core::config::AdapterPattern::CallbackBridge => {
                    let struct_key = format!("{}.__bridge_struct__", adapter.name);
                    let impl_key = format!("{}.__bridge_impl__", adapter.name);
                    if let Some(struct_code) = adapter_bodies.get(&struct_key) {
                        builder.add_item(struct_code);
                    }
                    if let Some(impl_code) = adapter_bodies.get(&impl_key) {
                        builder.add_item(impl_code);
                    }
                }
                _ => {}
            }
        }

        // NAPI has some unique patterns: Js-prefixed names, Option-wrapped fields,
        // and custom constructor. Use shared generators for enums and functions,
        // but keep struct/method generation custom.
        for typ in api
            .types
            .iter()
            .filter(|typ| !typ.is_trait && !exclude_types.contains(&typ.name))
        {
            // Capsule types bypass #[napi] class emission entirely — they are exposed
            // as raw External<T> pointers in JsObject wrappers from functions that return them.
            if capsule_types.contains_key(&typ.name) {
                continue;
            }
            if typ.is_opaque {
                // gen_opaque_struct_prefixed emits `#[napi]` from cfg.struct_attrs.
                // Replace with `#[napi(js_name = "Foo")]` so NAPI-RS exports the
                // unprefixed name while the Rust struct stays JsFoo internally.
                // Prepend `///` rustdoc so napi-derive forwards it to JSDoc on
                // the corresponding `export declare class` in index.d.ts.
                let opaque_struct_code = {
                    let raw = crate::codegen::generators::gen_opaque_struct_prefixed(typ, &cfg, &prefix);
                    let struct_name = format!("{prefix}{}", typ.name);
                    let body = raw.replace(
                        &format!("#[napi]pub struct {struct_name}"),
                        &format!("#[napi(js_name = \"{}\")]pub struct {struct_name}", typ.name),
                    );
                    let mut out = String::new();
                    let sanitized_doc = crate::codegen::doc_emission::sanitize_rust_idioms(
                        &typ.doc,
                        crate::codegen::doc_emission::DocTarget::TsDoc,
                    );
                    crate::codegen::doc_emission::emit_rustdoc(&mut out, &sanitized_doc, "");
                    out.push_str(&body);
                    out
                };
                builder.add_item(&opaque_struct_code);
                let capsule_type_names: AHashSet<String> = capsule_types.keys().cloned().collect();
                builder.add_item(&types::gen_opaque_struct_methods(
                    typ,
                    &mapper,
                    &cfg,
                    &opaque_types,
                    &prefix,
                    &adapter_bodies,
                    &streaming_item_types,
                    &capsule_type_names,
                    &mutex_types,
                    &capsule_types,
                ));
                // Client constructor — emit a #[napi(constructor)] impl
                if let Some(ctor) = config.client_constructors.get(&typ.name) {
                    let struct_name = format!("{prefix}{}", typ.name);
                    let ctor_body = crate::codegen::generators::gen_opaque_constructor(
                        ctor,
                        &typ.name,
                        &core_import,
                        "#[napi(constructor)]",
                    );
                    let ctor_impl = format!("#[napi]\nimpl {struct_name} {{\n{}}}", ctor_body);
                    builder.add_item(&ctor_impl);
                }
                // Constructors for opaque types: emit #[napi(constructor)] for default-constructors
                // or variant-wrapper constructors (but not both).
                // Variant-wrapper takes priority — when the type is referenced as the wrapper
                // of one or more registration variants, and variant bodies emit
                // `new WrapperType(args)` JS constructor-syntax, opt the wrapper's static
                // `new` into a `#[napi(constructor)]` so napi-rs exposes it as `new Class()`.
                //
                // `client_constructors` takes priority over both. Use a distinct Rust fn name
                // (`new_constructor`) to avoid a duplicate-`fn new` conflict with the
                // static `#[napi]` method already emitted by `gen_opaque_struct_methods`.
                //
                // CRITICAL: Do not emit #[napi(constructor)] fn new() if a static fn new()
                // already exists in typ.methods — this causes duplicate symbol errors.
                // Check has_static_new() before emitting.
                else if !config.client_constructors.contains_key(&typ.name) {
                    let has_static_new = typ.methods.iter().any(|m| m.is_static && m.name == "new");
                    if typ.is_variant_wrapper {
                        if let Some(ctor) = napi_variant_wrapper_constructor(typ, &mapper, &core_import, &prefix) {
                            builder.add_item(&ctor);
                        }
                    } else if typ.has_default && !has_static_new {
                        if let Some(ctor) = napi_default_constructor(typ, &mapper, &core_import, &prefix) {
                            builder.add_item(&ctor);
                        }
                    }
                }
                // Emit impl Default if type has has_default=true and a parameterless new() method.
                // This satisfies clippy's new_without_default lint.
                // Only emit if Default trait isn't already emitted elsewhere (e.g., derived on struct).
                if typ.has_default
                    && typ
                        .methods
                        .iter()
                        .any(|m| m.name == "new" && m.receiver.is_none() && m.params.is_empty())
                {
                    let struct_name = format!("{prefix}{}", typ.name);
                    let default_impl = format!(
                        "impl Default for {struct_name} {{\n    fn default() -> Self {{\n        Self::new()\n    }}\n}}\n"
                    );
                    builder.add_item(&default_impl);
                }
            } else {
                // Non-opaque structs use #[napi(object)] — plain JS objects without methods.
                // napi(object) structs cannot have #[napi] impl blocks.
                // gen_struct adds Default to derives when typ.has_default is true.
                builder.add_item(&types::gen_struct(
                    typ,
                    &mapper,
                    &prefix,
                    has_serde,
                    &opaque_types,
                    &never_skip_cfg_field_names,
                ));
                // Emit impl methods as standalone #[napi] free functions.
                // #[napi(object)] structs cannot have impl blocks, so each method becomes a
                // module-level function whose JS name encodes the type as a namespace prefix
                // (e.g. `processConfigAll`, `processConfigWithChunking`). TypeScript callers
                // group them via a value-namespace declaration in their own code.
                let dto_fns = types::gen_dto_method_fns(typ, &mapper, &cfg, &opaque_types, &prefix, &mutex_types, api);
                if !dto_fns.is_empty() {
                    builder.add_item(&dto_fns);
                }
            }
        }
        config_opaque::emit_wrappers(&mut builder, api, config, &capsule_types, &prefix);

        // Collect struct names so tagged enum codegen knows which Named types have binding structs
        let struct_names: ahash::AHashSet<String> = api.types.iter().map(|t| t.name.clone()).collect();

        // Collect Named types that have a Default impl. These are eligible to be
        // promoted to Option<T> in binding signatures so JS callers may pass
        // `undefined` to fall back to a default-constructed instance.
        let default_types: ahash::AHashSet<String> = api
            .types
            .iter()
            .filter(|t| t.has_default)
            .map(|t| t.name.clone())
            .collect();

        for enum_def in &api.enums {
            builder.add_item(&enums::gen_enum(enum_def, &prefix, has_serde));
        }

        let exclude_functions: ahash::AHashSet<String> = config
            .node
            .as_ref()
            .map(|c| c.exclude_functions.iter().cloned().collect())
            .unwrap_or_default();

        for func in &api.functions {
            if exclude_functions.contains(&func.name) {
                continue;
            }
            if crate::codegen::generators::trait_bridge::is_trait_bridge_managed_fn(&func.name, &config.trait_bridges) {
                continue;
            }
            let bridge_param = crate::backends::napi::trait_bridge::find_bridge_param(func, &config.trait_bridges);
            let options_field_bridge = crate::backends::napi::trait_bridge::find_options_field_binding(func, &config.trait_bridges)
                // Only use the options-field path when the bridge field actually survives
                // into the binding struct. If the core field is `#[cfg(...)]`-gated, the
                // struct generator strips it and the generated bridge code would reference
                // a missing field, producing `E0609 no field` at compile time.
                // Exception: fields listed in never_skip_cfg_field_names are cfg-gated but
                // preserved by the struct generator, so they are valid for bridge codegen.
                .filter(|(_, bridge_cfg)| {
                    let Some(field_name) = bridge_cfg.resolved_options_field() else { return false; };
                    let Some(options_type) = bridge_cfg.options_type.as_deref() else { return false; };
                    api.types
                        .iter()
                        .filter(|t| t.name == options_type)
                        .flat_map(|t| t.fields.iter())
                        .any(|f| f.name == field_name && (f.cfg.is_none() || never_skip_cfg_field_names.iter().any(|n| n == field_name)))
                });
            // Skip sanitized functions when there's no trait bridge that can replace the
            // sanitized parameter — such functions cannot be auto-delegated. Functions
            // whose only "sanitized" param is a configured trait_bridge param are emitted
            // via gen_bridge_function.
            if func.sanitized && bridge_param.is_none() && options_field_bridge.is_none() {
                continue;
            }
            // Prefer options_field binding over function-param binding for trait bridges.
            // When both exist, options_field provides the correct API surface (visitor
            // embedded in options object, not a separate parameter).
            if let Some((param_idx, bridge_cfg)) = options_field_bridge {
                let item = crate::backends::napi::trait_bridge::gen_options_field_bridge_function(
                    api,
                    func,
                    param_idx,
                    bridge_cfg,
                    &mapper,
                    &cfg,
                    &opaque_types,
                    &core_import,
                );
                let item = support::prepend_cfg(func.cfg.as_deref(), item);
                builder.add_item(&item);
            } else if let Some((param_idx, bridge_cfg)) = bridge_param {
                let item = crate::backends::napi::trait_bridge::gen_bridge_function(
                    api,
                    func,
                    param_idx,
                    bridge_cfg,
                    &mapper,
                    &cfg,
                    &Default::default(),
                    &opaque_types,
                    &core_import,
                );
                let item = support::prepend_cfg(func.cfg.as_deref(), item);
                builder.add_item(&item);
                let item = crate::backends::napi::trait_bridge::gen_options_field_bridge_function(
                    api,
                    func,
                    param_idx,
                    bridge_cfg,
                    &mapper,
                    &cfg,
                    &opaque_types,
                    &core_import,
                );
                let item = support::prepend_cfg(func.cfg.as_deref(), item);
                builder.add_item(&item);
            } else if !capsule_types.is_empty() && capsule::function_involves_capsule(func, &capsule_types) {
                // Function returns a capsule type — emit a napi shim that returns JsObject
                // with __parser = External<T>(ptr from value.into_raw()).
                // JsObjectValue provides set_named_property; imported once below.
                let item = capsule::gen_capsule_function(func, &capsule_types, &core_import);
                let item = support::prepend_cfg(func.cfg.as_deref(), item);
                builder.add_item(&item);
            } else {
                let item = functions::gen_function(
                    func,
                    &mapper,
                    &cfg,
                    &opaque_types,
                    &default_types,
                    &prefix,
                    &capsule_types,
                    &mutex_types,
                );
                let item = support::prepend_cfg(func.cfg.as_deref(), item);
                builder.add_item(&item);
            }
        }

        // Emit module-level wrapper functions for adapters (streaming methods).
        for adapter in &config.adapters {
            builder.add_item(&functions::gen_adapter_wrapper(adapter, &core_import, &api.types));
        }

        // Service-API glue lives in the generated `service.rs`; declare it so its
        // `#[napi]` entrypoints (e.g. `app_run`) are compiled and exported.
        if !api.services.is_empty() {
            builder.add_item("pub mod service;");
        }

        // Trait bridge wrappers — generate NAPI bridge structs that delegate to JS objects
        for bridge_cfg in &config.trait_bridges {
            if let Some(trait_type) = api.types.iter().find(|t| t.is_trait && t.name == bridge_cfg.trait_name) {
                let bridge = crate::backends::napi::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);
            }
        }

        let binding_to_core = crate::codegen::conversions::convertible_types(api);
        let core_to_binding = crate::codegen::conversions::core_to_binding_convertible_types(api);
        let input_types = crate::codegen::conversions::input_type_names(api);
        // NOTE: NAPI does NOT populate `trait_bridge_arc_wrapper_field_names`. Unlike
        // PHP/WASM which wrap their visitor handle as `WrapperType { inner: Arc<...> }`,
        // the NAPI binding stores the raw JS `napi::bindgen_prelude::Object` directly on
        // `JsConversionOptions.visitor`. There is no `.inner` field to dereference, so
        // the `(*v.inner).clone()` substitution would emit code that fails to compile.
        // Instead, the NAPI `convert` codegen attaches the visitor in a post-process
        // step after the `From<JsConversionOptions>` impl runs (`o.visitor = None;`
        // then `result.visitor = visitor_handle.clone()`), so the From impl harmlessly
        // emits `Default::default()` for the visitor field.
        let napi_conv_config = crate::codegen::conversions::ConversionConfig {
            type_name_prefix: &prefix,
            cast_large_ints_to_i64: true,
            cast_f32_to_f64: true,
            // optionalize_defaults: For types with has_default, conversion generators
            // make all fields Option<T> and apply defaults via FromNapiValue,
            // enabling JS users to pass partial objects and omit fields they want defaults for.
            optionalize_defaults: true,
            option_duration_on_defaults: true,
            include_cfg_metadata: true,
            // Pass opaque_types so the conversion generator can emit `Default::default()`
            // for opaque-type fields (e.g. visitor: Object<'static>) instead of trying to
            // convert them via Into — these fields are handled separately via bridge code.
            opaque_types: Some(&opaque_types),
            // Json fields are stored as serde_json::Value in the binding so JS
            // callers can pass objects/arrays/scalars directly.
            json_as_value: true,
            never_skip_cfg_field_names: &never_skip_cfg_field_names,
            ..Default::default()
        };
        // From/Into conversions using shared parameterized generators.
        // Exclude Builder/Update DTOs — their struct definitions are filtered out of
        // emission upstream, so emitting `From<JsXxxUpdate> for core::XxxUpdate` would
        // reference an undefined `JsXxxUpdate` type.
        for typ in api
            .types
            .iter()
            .filter(|typ| !typ.is_trait)
            .filter(|typ| !typ.name.ends_with("Builder") && !typ.name.ends_with("Update"))
        {
            if input_types.contains(&typ.name)
                && crate::codegen::conversions::can_generate_conversion(typ, &binding_to_core)
            {
                builder.add_item(&crate::codegen::conversions::gen_from_binding_to_core_cfg(
                    typ,
                    &core_import,
                    &napi_conv_config,
                ));
            }
            if crate::codegen::conversions::can_generate_conversion(typ, &core_to_binding) {
                builder.add_item(&crate::codegen::conversions::gen_from_core_to_binding_cfg(
                    typ,
                    &core_import,
                    &opaque_types,
                    &napi_conv_config,
                ));
            }
        }
        let mut emitted_enum_binding_to_core: AHashSet<String> = AHashSet::new();
        for e in &api.enums {
            let has_data_variants = e.variants.iter().any(|v| !v.fields.is_empty());
            let is_tagged_data_enum = e.serde_tag.is_some() && has_data_variants;
            let is_untagged_data_enum = e.serde_untagged && has_data_variants;
            if is_tagged_data_enum {
                // Tagged data enums use flattened struct — generate custom conversions
                builder.add_item(&methods::gen_tagged_enum_binding_to_core(
                    e,
                    &core_import,
                    &prefix,
                    &struct_names,
                ));
                builder.add_item(&methods::gen_tagged_enum_core_to_binding(
                    e,
                    &core_import,
                    &prefix,
                    &struct_names,
                ));
            } else if is_untagged_data_enum {
                // Untagged data enums are wrapped around serde_json::Value — bridge via serde.
                let binding_name = format!("{prefix}{}", e.name);
                let core_path = crate::codegen::conversions::core_enum_path_remapped(
                    e,
                    &core_import,
                    napi_conv_config.source_crate_remaps,
                );
                builder.add_item(&format!(
                    "impl From<{binding_name}> for {core_path} {{\n    \
                         fn from(val: {binding_name}) -> Self {{\n        \
                             serde_json::from_value(val.0).unwrap_or_default()\n    \
                         }}\n\
                     }}\n"
                ));
                builder.add_item(&format!(
                    "impl From<{core_path}> for {binding_name} {{\n    \
                         fn from(val: {core_path}) -> Self {{\n        \
                             Self(serde_json::to_value(val).unwrap_or_default())\n    \
                         }}\n\
                     }}\n"
                ));
            } else {
                if input_types.contains(&e.name) && crate::codegen::conversions::can_generate_enum_conversion(e) {
                    builder.add_item(&crate::codegen::conversions::gen_enum_from_binding_to_core_cfg(
                        e,
                        &core_import,
                        &napi_conv_config,
                    ));
                    emitted_enum_binding_to_core.insert(e.name.clone());
                }
                if crate::codegen::conversions::can_generate_enum_conversion_from_core(e) {
                    builder.add_item(&crate::codegen::conversions::gen_enum_from_core_to_binding_cfg(
                        e,
                        &core_import,
                        &napi_conv_config,
                    ));
                }
            }
        }

        // From impls for tagged data enums lowered to flat NAPI classes.
        // Track types whose `From<binding> for core` impl has already been emitted by
        // the main loop above (or by a prior variant in this loop) to avoid duplicate
        // impls when the same DTO appears both as a top-level input type and as a
        // variant payload of a tagged enum (e.g. `CrawlPageResult` used directly and
        // inside `CrawlEvent::Page { result: Box<CrawlPageResult> }`).
        // The main loop above emits a `From<binding> for core` impl for any type
        // that is `input_types.contains(&typ.name)`. Pre-seed the dedup set with those.
        let mut emitted_binding_to_core: AHashSet<String> = api
            .types
            .iter()
            .filter(|typ| !typ.is_trait && input_types.contains(&typ.name))
            .filter(|typ| !typ.name.ends_with("Builder") && !typ.name.ends_with("Update"))
            .filter(|typ| crate::codegen::conversions::can_generate_conversion(typ, &binding_to_core))
            .map(|typ| typ.name.clone())
            .collect();
        for enum_def in api.enums.iter() {
            let has_data_variants = enum_def.variants.iter().any(|v| !v.fields.is_empty());
            let is_tagged_data_enum = enum_def.serde_tag.is_some() && has_data_variants;
            if !is_tagged_data_enum {
                continue;
            }
            // Also generate From impls for variant data types (e.g., BibtexMetadata from Metadata::Bibtex).
            // These are needed when tagged enum binding→core conversion calls `.into()` on variant fields.
            for variant in &enum_def.variants {
                for field in &variant.fields {
                    if let TypeRef::Named(type_name) = &field.ty {
                        if let Some(typ) = api.types.iter().find(|t| &t.name == type_name) {
                            if emitted_binding_to_core.contains(&typ.name) {
                                continue;
                            }
                            if crate::codegen::conversions::can_generate_conversion(typ, &binding_to_core) {
                                builder.add_item(&crate::codegen::conversions::gen_from_binding_to_core_cfg(
                                    typ,
                                    &core_import,
                                    &napi_conv_config,
                                ));
                                emitted_binding_to_core.insert(typ.name.clone());
                            }
                        }
                    }
                }
            }
        }

        // Emit From impls for all remaining DTO types that are convertible but haven't been
        // emitted yet. This handles nested types that appear as fields in output structures
        // but are not direct input types or enum variant payloads (e.g., DbfFieldInfo inside
        // Vec<DbfFieldInfo> in DbfMetadata, which itself is inside FormatMetadata::Dbf).
        for typ in api
            .types
            .iter()
            .filter(|t| !t.is_trait)
            .filter(|t| !t.name.ends_with("Builder") && !t.name.ends_with("Update"))
        {
            if !emitted_binding_to_core.contains(&typ.name)
                && crate::codegen::conversions::can_generate_conversion(typ, &binding_to_core)
            {
                builder.add_item(&crate::codegen::conversions::gen_from_binding_to_core_cfg(
                    typ,
                    &core_import,
                    &napi_conv_config,
                ));
                emitted_binding_to_core.insert(typ.name.clone());
            }
        }

        // Emit From impls for unit-variant enums that appear in fields of types we've
        // already emitted From impls for. These are needed because binding code calls
        // `.map(Into::into)` on Option<enum> fields (e.g., Option<TextDirection>).
        for typ in api
            .types
            .iter()
            .filter(|t| !t.is_trait && emitted_binding_to_core.contains(&t.name))
        {
            for field in &typ.fields {
                // Recursively extract enum names from field types (e.g., Option<TextDirection> → TextDirection)
                fn collect_enum_names(ty: &TypeRef, enums: &mut AHashSet<String>) {
                    match ty {
                        TypeRef::Named(name) => {
                            enums.insert(name.clone());
                        }
                        TypeRef::Optional(inner) | TypeRef::Vec(inner) => collect_enum_names(inner, enums),
                        TypeRef::Map(_k, v) => collect_enum_names(v, enums),
                        _ => {}
                    }
                }
                let mut field_enums = AHashSet::new();
                collect_enum_names(&field.ty, &mut field_enums);
                for enum_name in field_enums {
                    if let Some(enum_def) = api.enums.iter().find(|e| e.name == enum_name) {
                        // Skip data enums — they have their own conversion logic via tagged enum handling
                        let has_data_variants = enum_def.variants.iter().any(|v| !v.fields.is_empty());
                        if enum_def.serde_tag.is_some() && has_data_variants {
                            continue;
                        }
                        if enum_def.serde_untagged && has_data_variants {
                            continue;
                        }
                        // Unit-variant enum: emit From impl for binding→core if not already emitted
                        if !emitted_enum_binding_to_core.contains(&enum_def.name)
                            && crate::codegen::conversions::can_generate_enum_conversion(enum_def)
                        {
                            builder.add_item(&crate::codegen::conversions::gen_enum_from_binding_to_core_cfg(
                                enum_def,
                                &core_import,
                                &napi_conv_config,
                            ));
                            emitted_enum_binding_to_core.insert(enum_def.name.clone());
                        }
                    }
                }
            }
        }

        // Error types (variant name constants + converter functions)
        for error in &api.errors {
            builder.add_item(&crate::codegen::error_gen::gen_napi_error_types(error));
            builder.add_item(&crate::codegen::error_gen::gen_napi_error_converter(
                error,
                &core_import,
            ));
            // Emit #[napi] class for errors with introspection methods.
            let class_code = crate::codegen::error_gen::gen_napi_error_class(error, &core_import);
            if !class_code.is_empty() {
                builder.add_item(&class_code);
            }
        }

        let mut content = builder.build();

        // Post-process: Fix From<JsXxx> (binding to core) impls to forward visitor field.
        // The conversion generator emits `__result.visitor = Default::default();` in binding→core
        // conversions because the raw JS napi::bindgen_prelude::Object is not Clone-able.
        // This post-process detects that pattern in the JS→Rust direction and replaces it with
        // code that wraps the configured field into the generated bridge and then into the core handle type.
        //
        // Key: only fix `impl From<Js{type}>` (binding→core), NOT `impl From<core_type>` (core→binding).
        // The core→binding direction correctly uses Default because the Rc<RefCell<>> is opaque to JS.
        for bridge in &config.trait_bridges {
            if bridge.bind_via != crate::core::config::BridgeBinding::OptionsField {
                continue;
            }
            if let Some(field_name) = bridge.resolved_options_field() {
                // Verify the field is present in the binding struct. The NAPI backend keeps
                // cfg-gated fields in the binding (decorated with `#[cfg(...)]`) when their
                // names are in `never_skip_cfg_field_names` — every trait-bridge OptionsField
                // is registered there above (line 114-125), so accept cfg-gated fields too.
                let Some(options_type) = bridge.options_type.as_deref() else {
                    continue;
                };
                let field_in_binding = api
                    .types
                    .iter()
                    .filter(|t| t.name == options_type)
                    .flat_map(|t| t.fields.iter())
                    .any(|f| {
                        f.name == field_name
                            && (f.cfg.is_none() || never_skip_cfg_field_names.iter().any(|n| n == field_name))
                    });
                if !field_in_binding {
                    continue;
                }

                // Find the binding→core conversion impl: `impl From<Js{options_type}> for core...`
                let prefix = config.node_type_prefix();
                let js_type_name = format!("{prefix}{options_type}");
                let impl_marker = format!("impl From<{js_type_name}> for {core_import}");

                // Search forward from the impl marker to find its closing brace and visitor wipe.
                // We only fix the impl that converts FROM the JS binding type.
                if let Some(impl_start) = content.find(&impl_marker) {
                    // Find the matching closing brace for this impl block
                    let from_impl_start = impl_start;
                    let impl_body = &content[from_impl_start..];

                    // Find the next `}` that closes this impl — being careful to count braces
                    let mut brace_depth = 0;
                    let mut impl_end = 0;
                    let mut found_fn_from = false;
                    for (i, ch) in impl_body.char_indices() {
                        if ch == '{' {
                            brace_depth += 1;
                            // Once we see the opening brace of `fn from(...) {`, mark it
                            if impl_body[..i].contains("fn from") {
                                found_fn_from = true;
                            }
                        } else if ch == '}' {
                            brace_depth -= 1;
                            if brace_depth == 0 && found_fn_from {
                                impl_end = i;
                                break;
                            }
                        }
                    }

                    if impl_end > 0 {
                        let impl_block = &impl_body[..impl_end];
                        let pattern = format!("__result.{field_name} = Default::default();");

                        if let Some(rel_pos) = impl_block.find(&pattern) {
                            let pos = from_impl_start + rel_pos;
                            let before = &content[..pos];
                            let after = &content[pos + pattern.len()..];

                            // Build the replacement that wraps the configured field into the configured bridge.
                            let handle_path =
                                crate::codegen::generators::trait_bridge::bridge_handle_path(api, bridge, &core_import);
                            let struct_name =
                                crate::codegen::generators::trait_bridge::bridge_wrapper_name("Js", bridge);
                            let replacement = format!(
                                "__result.{field_name} = val.{field_name}.and_then(|obj| {{\n            \
                                    {struct_name}::new(obj).ok().map(|bridge| {{\n                \
                                        std::sync::Arc::new(std::sync::Mutex::new(bridge)) as {handle_path}\n            \
                                    }})\n        \
                                }});"
                            );

                            content = format!("{}{}{}", before, replacement, after);
                        }
                    }
                }
            }
        }

        let output_dir = resolve_output_dir(config.output_paths.get("node"), &config.name, "crates/{name}-node/src/");

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

    fn generate_type_stubs(
        &self,
        api: &ApiSurface,
        config: &ResolvedCrateConfig,
    ) -> anyhow::Result<Vec<GeneratedFile>> {
        type_stubs::generate(api, config)
    }

    fn generate_public_api(
        &self,
        api: &ApiSurface,
        config: &ResolvedCrateConfig,
    ) -> anyhow::Result<Vec<GeneratedFile>> {
        public_api::generate(api, config)
    }

    fn generate_service_api(
        &self,
        api: &ApiSurface,
        config: &ResolvedCrateConfig,
    ) -> anyhow::Result<Vec<GeneratedFile>> {
        service_api::generate(api, config)
    }

    fn build_config(&self) -> Option<BuildConfig> {
        Some(BuildConfig {
            tool: "napi",
            crate_suffix: "-node",
            build_dep: BuildDependency::None,
            post_build: vec![PostBuildStep::PatchFile {
                path: "index.d.ts",
                find: "export declare const enum",
                replace: "export declare enum",
            }],
        })
    }
}
