use crate::codegen::keywords::{swift_case_ident, swift_ident};
use crate::codegen::shared::binding_fields;
use crate::codegen::type_mapper::TypeMapper;
use crate::core::backend::{Backend, BuildConfig, BuildDependency, Capabilities, GeneratedFile, PostBuildStep};
use crate::core::config::{
    AdapterConfig, AdapterPattern, BridgeBinding, Language, ResolvedCrateConfig, TraitBridgeConfig, resolve_output_dir,
};
use crate::core::ir::{
    ApiSurface, DefaultValue, EnumDef, EnumVariant, ErrorDef, FunctionDef, MethodDef, PrimitiveType, TypeDef, TypeRef,
};
use heck::{AsSnakeCase, ToKebabCase, ToLowerCamelCase, ToSnakeCase, ToUpperCamelCase};
use std::collections::BTreeSet;
use std::path::PathBuf;

use crate::backends::swift::gen_rust_crate;
use crate::backends::swift::type_map::SwiftMapper;

pub mod trait_bridge;

pub struct SwiftBackend;

fn effective_exclude_types(config: &ResolvedCrateConfig) -> std::collections::HashSet<String> {
    let mut exclude_types: std::collections::HashSet<String> = config
        .ffi
        .as_ref()
        .map(|c| c.exclude_types.iter().cloned().collect())
        .unwrap_or_default();
    if let Some(swift) = &config.swift {
        exclude_types.extend(swift.exclude_types.iter().cloned());
    }
    exclude_types
}

impl Backend for SwiftBackend {
    fn name(&self) -> &str {
        "swift"
    }

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

    fn capabilities(&self) -> Capabilities {
        Capabilities {
            supports_async: true,
            supports_classes: true,
            supports_enums: true,
            supports_option: true,
            supports_result: true,
            supports_callbacks: false,
            supports_streaming: true,
        }
    }

    fn generate_bindings(&self, api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
        let module_name = config.swift_module();
        let mapper = SwiftMapper;

        // Function-wrapper emission is disabled in this phase (see comment below);
        // `swift.exclude_functions` therefore has no effect on the host wrapper but
        // is still consumed by the Rust-side bridge crate via gen_rust_crate::emit.
        let exclude_types = effective_exclude_types(config);
        // The Rust-side gen_rust_crate uses the same `exclude_fields` set to gate which
        // fields appear in the `#[swift_bridge(init)] fn new(...)` constructor extern.
        // We mirror that set here so `intoRust()` knows when a bulk constructor extern
        // is available and which fields it accepts.
        let exclude_fields: std::collections::HashSet<String> = config
            .swift
            .as_ref()
            .map(|c| c.exclude_fields.iter().cloned().collect())
            .unwrap_or_default();

        let mut imports: BTreeSet<String> = BTreeSet::new();
        // Foundation is always included — Codable, Data, URL all live there.
        imports.insert("import Foundation".to_string());
        // RustBridge is the Swift module generated by swift-bridge from the Rust bridge crate.
        // It provides opaque type definitions and FFI entry points.
        if !api.types.is_empty() || !api.enums.is_empty() || !api.errors.is_empty() {
            imports.insert("import RustBridge".to_string());
        }

        let mut body = String::new();

        // Unit serde enums (all-unit + has_serde) are emitted as `public enum S: String, Codable`
        // with serde-derived raw values. They are Codable so structs containing them as fields
        // can also derive Codable. Collect their names before the fixed-point loop so the loop
        // can accept `Named(enum_S)` fields.
        let unit_serde_enum_names: std::collections::HashSet<String> = api
            .enums
            .iter()
            .filter(|e| !exclude_types.contains(&e.name))
            .filter(|e| e.has_serde && e.variants.iter().all(|v| v.fields.is_empty()))
            .map(|e| e.name.clone())
            .collect();

        // Compute which struct DTO types will actually be emitted as first-class Swift
        // structs via a fixed-point iteration.
        //
        // A type is first-class iff:
        //   1. It is non-opaque, has_serde, non-trait, non-excluded, and has visible fields.
        //   2. Every visible field's TypeRef is "supported" given the current first-class set.
        //
        // Unit serde enums (above) are Codable and seed the initial `known_dto_names` set.
        // Struct DTOs are added iteratively: a struct becomes first-class when all its fields
        // are known-supported (primitive, String, known first-class struct, or unit serde enum).
        //
        // Convergence is guaranteed: each round only adds types (monotonically growing set).
        let candidate_types: Vec<&crate::core::ir::TypeDef> = api
            .types
            .iter()
            .filter(|t| !t.is_trait && !t.is_opaque && t.has_serde && !exclude_types.contains(&t.name))
            .filter(|t| !t.fields.is_empty())
            .collect();

        // Collect every serde data-variant enum (both untagged AND tagged) that bridges
        // as a JSON `RustString` at the FFI boundary rather than as an opaque
        // `RustBridge.{Name}Ref`. swift-bridge does not emit an `init(_ rb:Ref)` for
        // either form — untagged enums (`#[serde(untagged)]`) are emitted directly as
        // JSON, and tagged enums (default external tagging or `#[serde(tag = "…")]`)
        // are also returned as JSON strings from the bridge accessor. Containing structs
        // must decode these via JSONDecoder rather than via the broken `try {Name}(rb.field())`
        // path. The set name is kept as `untagged_enum_names` for legacy continuity; it
        // captures any Codable enum with data variants whose bridge accessor returns
        // RustString and whose Swift target type only has `init(from: Decoder)`.
        let untagged_enum_names: std::collections::HashSet<String> = api
            .enums
            .iter()
            .filter(|e| !exclude_types.contains(&e.name))
            .filter(|e| e.has_serde && e.variants.iter().any(|v| !v.fields.is_empty()))
            .map(|e| e.name.clone())
            .collect();

        // Seed with unit serde enum names + every data-variant Codable enum name (tagged +
        // untagged, all collected above). All are Codable and can appear as struct fields.
        // They are included so that `first_class_field_supported` accepts them as valid
        // field types, allowing containing structs to be emitted as first-class Swift
        // structs. The init code then routes Named fields of these types through
        // JSONDecoder rather than the Ref path.
        let mut known_dto_names: std::collections::HashSet<String> = unit_serde_enum_names.clone();
        known_dto_names.extend(untagged_enum_names.iter().cloned());
        loop {
            let prev_len = known_dto_names.len();
            for ty in &candidate_types {
                if known_dto_names.contains(&ty.name) {
                    continue;
                }
                // Check if all visible (binding-non-excluded) fields are supported given the
                // current known set (struct DTOs + unit serde enums + tagged enums).
                let all_supported =
                    binding_fields(&ty.fields).all(|field| first_class_field_supported(&field.ty, &known_dto_names));
                if all_supported {
                    known_dto_names.insert(ty.name.clone());
                }
            }
            if known_dto_names.len() == prev_len {
                break; // stable — no new types added this round
            }
        }

        // Emit typealiases for all struct types exposed by swift-bridge.
        // swift-bridge exposes types that are declared in the extern "Rust" block,
        // so we generate typealiases for all non-excluded types to provide a
        // stable Swift API that references RustBridge types.
        // Skip opaque handle types with methods — those get a full class wrapper below
        // instead of a typealias.
        for ty in api
            .types
            .iter()
            .filter(|t| !t.is_trait && !exclude_types.contains(&t.name))
            .filter(|t| t.methods.is_empty() || !t.is_opaque && t.has_serde)
        {
            emit_doc_comment(&ty.doc, "", &mut body);
            if can_emit_first_class_struct(ty, &mapper, &exclude_fields, &known_dto_names) {
                emit_first_class_struct(
                    ty,
                    &mapper,
                    &exclude_fields,
                    &known_dto_names,
                    &unit_serde_enum_names,
                    &untagged_enum_names,
                    &mut body,
                );
            } else {
                body.push_str(&crate::backends::swift::template_env::render(
                    "typealias.jinja",
                    minijinja::context! {
                        name => &ty.name,
                    },
                ));
            }
            body.push('\n');
        }

        // Collect result enum names from trait bridges — these are emitted as first-class
        // Swift enums that JSON-decode locally, so they don't call a Rust-side from_json fn.
        let result_type_enums: std::collections::HashSet<String> = config
            .trait_bridges
            .iter()
            .filter_map(|b| b.result_type.as_deref().map(|s| s.to_string()))
            .collect();

        // Enums are emitted as native Swift enums with unit variants only.
        // They are NOT typealiased to RustBridge since swift-bridge's automatic generation
        // is unreliable (not all enum types are exposed). Instead, we emit them directly
        // as Swift enums that mirror the unit variants from the Rust bridge enum wrapper.
        for en in api.enums.iter().filter(|e| !exclude_types.contains(&e.name)) {
            if result_type_enums.contains(&en.name) {
                // Result-type enums are emitted without intoRust() because Swift decodes them
                // locally via JSONDecoder, not via a Rust-side from_json function.
                emit_enum_without_into_rust(en, &mut body, &mapper, &known_dto_names);
            } else {
                emit_enum(en, &mut body, &mapper, &known_dto_names);
            }
            body.push('\n');
        }

        for error in &api.errors {
            emit_error(error, &module_name, &mut body, &mapper);
            body.push('\n');
        }

        // Emit Swift class wrappers only for opaque handle types that expose methods
        // AND have an explicit `client_constructor_body` in alef.toml.  Without an
        // override the Rust bridge crate does not emit a `create_<type>` shim
        // (see gen_rust_crate::mod.rs), so a class wrapper with `init(apiKey,
        // baseUrl)` would reference a non-existent bridge function.
        // Opaque types without an override are returned by Rust APIs, not
        // constructed in Swift — they remain accessible via their `RustBridge`
        // typealiases / direct method shims.
        let client_constructor_types: std::collections::HashSet<&str> = config
            .swift
            .as_ref()
            .map(|c| c.client_constructor_body.keys().map(String::as_str).collect())
            .unwrap_or_default();
        // Collect first-class struct type names for streaming chunk decode dispatch.
        // Must match the actual emission decision in `can_emit_first_class_struct`
        // (Codable struct with all-supported fields).
        // Types failing that check are emitted as typealiases to RustBridge.X and so
        // do not have a `func intoRust()` extension or auto-derived Codable conformance.
        let first_class_types: std::collections::HashSet<String> = api
            .types
            .iter()
            .filter(|t| !t.is_trait && !exclude_types.contains(&t.name))
            .filter(|t| can_emit_first_class_struct(t, &mapper, &exclude_fields, &known_dto_names))
            .map(|t| t.name.clone())
            .collect();
        // Tracks every type that has already received an `extension RustBridge.X:
        // @unchecked Sendable {}` so the streaming-specific emissions and the
        // catch-all block below never declare the conformance twice (Swift warns
        // on a redundant conformance).
        let mut sendable_emitted: std::collections::HashSet<String> = std::collections::HashSet::new();
        for ty in api.types.iter().filter(|t| {
            !t.is_trait
                && !exclude_types.contains(&t.name)
                && !t.methods.is_empty()
                && (t.is_opaque || !t.has_serde)
                && client_constructor_types.contains(t.name.as_str())
        }) {
            emit_client_class(
                ty.name.as_str(),
                &ty.methods,
                &mapper,
                config,
                &first_class_types,
                &mut body,
            );
            body.push('\n');
            // Emit `@unchecked Sendable` conformance for every StreamHandle owned by this
            // client type.  swift-bridge emits opaque types as plain Swift classes without
            // `Sendable`; Swift 6 strict-concurrency rejects passing them across
            // `Task.detached` boundaries.  The Rust side wraps a Mutex<stream> and a
            // tokio Runtime — both thread-safe — so `@unchecked` is correct.
            // Collect streaming adapters owned by this client type.
            let streaming_adapters: Vec<&AdapterConfig> = config
                .adapters
                .iter()
                .filter(|a| matches!(a.pattern, AdapterPattern::Streaming))
                .filter(|a| !a.skip_languages.iter().any(|l| l == "swift"))
                .filter(|a| a.owner_type.as_deref() == Some(ty.name.as_str()))
                .collect();
            if !streaming_adapters.is_empty() {
                // The streaming methods capture `self.inner` (typed `RustBridge.{ty.name}`)
                // inside a `Task.detached` closure.  Swift 6 strict-concurrency requires every
                // captured value to be `Sendable`.  The Rust type is `Send + Sync`, so
                // `@unchecked` is correct.
                let inner_ty = ty.name.as_str();
                if sendable_emitted.insert(inner_ty.to_string()) {
                    body.push_str(&format!(
                        "// MARK: - Sendable conformance for {inner_ty} (streaming client inner)\n"
                    ));
                    body.push_str("// swift-bridge opaque types are not automatically Sendable.\n");
                    body.push_str("// Captured by Task.detached in streaming methods — Rust type is Send + Sync.\n");
                    body.push_str(&format!("extension RustBridge.{inner_ty}: @unchecked Sendable {{}}\n"));
                }
            }
            for adapter in &streaming_adapters {
                emit_stream_handle_sendable(adapter, ty.name.as_str(), &mut body);
            }
            // Emit `@unchecked Sendable` conformance for every opaque param type
            // used in streaming adapter calls.  These types are passed into
            // `Task.detached` closures; Swift 6 strict-concurrency requires them
            // to be `Sendable`.  They are swift-bridge opaque classes backed by
            // Rust types that are `Send + Sync`, so `@unchecked` is correct.
            {
                for adapter in &streaming_adapters {
                    for param in &adapter.params {
                        let simple_ty = param.ty.rsplit("::").next().unwrap_or(&param.ty).to_string();
                        if sendable_emitted.insert(simple_ty.clone()) {
                            body.push_str(&format!(
                                "// MARK: - Sendable conformance for {simple_ty} (streaming request param)\n"
                            ));
                            body.push_str("// swift-bridge opaque types are not automatically Sendable.\n");
                            body.push_str("// Passed into Task.detached for streaming — Rust type is Send + Sync.\n");
                            body.push_str(&format!("extension RustBridge.{simple_ty}: @unchecked Sendable {{}}\n"));
                        }
                    }
                }
            }
        }

        // Emit lightweight convenience overloads for functions whose first parameter
        // is TypeRef::Bytes or TypeRef::Path. Discovery is IR-driven: the emitter
        // walks api.functions and detects candidates by signature shape, never by name.
        emit_convenience_wrappers(api, &mut body);

        // Emit public Swift forwarding functions for `*FromJson` helpers.
        // The Rust bridge crate exposes `{type_snake}_from_json` as a swift-bridge
        // free function accessible via `RustBridge.{swiftName}(json)`. Without a
        // forwarding wrapper in this module, callers would need the `RustBridge.`
        // prefix, which the generated e2e test layer doesn't use.
        emit_from_json_forwarders(
            api,
            &exclude_types,
            &mapper,
            &exclude_fields,
            &known_dto_names,
            &mut body,
        );

        // Emit Swift protocol + adapter class for OptionsField inbound trait bridges.
        // Each bridge in `config.trait_bridges` where `bind_via = "options_field"` gets:
        //   - A `public protocol {Trait}Protocol: AnyObject` with one method per trait method.
        //   - A private adapter class that wraps the protocol conformer.
        //   - A factory func `make{TraitCamel}Handle(...)`.
        // NOTE: The `Swift{Trait}Box` class is emitted into Sources/RustBridge/ (separate file)
        // because it is referenced by the swift-bridge generated @_cdecl shims there.
        emit_inbound_protocols(api, config, &mut body);

        // Emit top-level public forwarders for every free function in `api.functions`
        // that is not already covered by the bytes/path convenience wrappers or
        // the `*FromJson` forwarders. Without these,
        // `embedTexts`, `listEmbeddingPresets`, `getEmbeddingPreset`, `renderPdfPageToPng`,
        // `getExtensionsForMime`, `detectMimeType`, and the various `list_*` registry
        // helpers are only reachable as `RustBridge.fnName(...)` — which requires
        // consumers of the `Kreuzberg` module to also `import RustBridge`, violating
        // the alef binding-parity promise that every public Rust free function is
        // re-exposed as a top-level public function on the host module.
        let client_class_names: std::collections::HashSet<String> =
            client_constructor_types.iter().map(|&s| s.to_string()).collect();
        emit_free_function_forwarders(api, config, &known_dto_names, &client_class_names, &mut body);

        // Emit top-level public forwarders for trait-bridge register/unregister/clear
        // entry points. These are synthesised from `[[trait_bridges]]` in alef.toml
        // (not present in `api.functions`), so the free-function forwarder pass above
        // does not reach them. Mirrors the extendr/napi/php fixes — every native
        // binding exposes `register_<trait>` / `unregister_<trait>` / `clear_<trait>`
        // as part of the public plugin registration surface.
        emit_trait_bridge_forwarders(config, &mut body);

        // Emit AsyncThrowingStream free functions for streaming adapters whose owner
        // type has no Swift client class wrapper (no client_constructor_body in alef.toml).
        // For these adapters, emit_client_class is never called so the streaming methods
        // must be top-level free functions taking the handle as the first parameter.
        emit_streaming_free_functions(config, &first_class_types, &mut body);

        // Emit `@unchecked Sendable` conformance for all remaining opaque types.
        // swift-bridge emits opaque types as plain Swift classes without `Sendable`
        // conformance. Swift 6 strict-concurrency rejects passing non-Sendable values
        // across `Task.detached` boundaries, which occurs in async function forwarders
        // that wrap blocking Rust calls. All swift-bridge opaque types wrap Rust
        // pointers to Send + Sync types, so `@unchecked` is correct for all of them.
        //
        // The set covers both genuinely-opaque IR types and types returned across the
        // bridge as opaque handles (`compute_handle_returned_types`). The latter is
        // essential: a result struct such as `ScrapeResult` may also be emitted as a
        // first-class Swift struct, but it is still bridged as an opaque
        // `RustBridge.ScrapeResult` class that an async forwarder returns out of a
        // `Task.detached`. `sendable_emitted` prevents re-declaring a conformance the
        // streaming emissions above already produced.
        {
            let handle_returned =
                crate::backends::swift::gen_rust_crate::type_bridge::compute_handle_returned_types(api);
            for ty in api.types.iter().filter(|t| {
                !t.is_trait && !exclude_types.contains(&t.name) && (t.is_opaque || handle_returned.contains(&t.name))
            }) {
                if sendable_emitted.insert(ty.name.clone()) {
                    body.push_str("// swift-bridge opaque type used across Task.detached boundaries — Rust type is Send + Sync.\n");
                    body.push_str(&format!("extension RustBridge.{}: @unchecked Sendable {{}}\n", ty.name));
                }
            }
        }

        let mut content = String::new();
        content.push_str("// Generated by alef. Do not edit by hand.\n");
        // swift-format-ignore-file tells swift-format to skip this file entirely.
        // Generated Swift code violates several formatting rules (long lines,
        // trailing commas in function signatures, etc.) that cannot be trivially
        // fixed without a full reformatter post-processing step.
        content.push_str("// swift-format-ignore-file\n\n");
        for import in &imports {
            content.push_str(import);
            content.push('\n');
        }
        content.push('\n');
        content.push_str(&body);

        let base_dir = resolve_output_dir(config.output_paths.get("swift"), &config.name, "packages/swift");
        let base_path = PathBuf::from(&base_dir);
        // Explicit `[crates.output] swift = "..."` is treated as the final
        // package directory: the user controls layout. Without an override the
        // backend constructs the canonical SwiftPM `Sources/<Module>/` layout
        // under the template-derived base.
        let path = if config.explicit_output.swift.is_some() {
            base_path.join(format!("{module_name}.swift"))
        } else {
            base_path
                .join("Sources")
                .join(&module_name)
                .join(format!("{module_name}.swift"))
        };

        let mut files = vec![GeneratedFile {
            path,
            content,
            generated_header: false,
        }];

        // Phase 2C: emit the Rust-side swift-bridge crate
        let rust_crate_files = gen_rust_crate::emit(api, config)?;
        files.extend(rust_crate_files);

        // Phase 2D: if the swift-bridge build output exists in target/*/out/, emit the
        // properly prefixed bridge files into Sources/RustBridge{,C}/.
        // These files are generated by `cargo build -p {crate}-swift` via
        // swift_bridge_build and must be present in the SwiftPM package for it to compile.
        let binding_crate_name = format!("{}-swift", &api.crate_name);
        let base_dir = resolve_output_dir(config.output_paths.get("swift"), &config.name, "packages/swift");
        let package_root = PathBuf::from(&base_dir)
            .ancestors()
            // walk up until we find Sources/ sibling, or stop at root
            .find(|p| p.join("Sources").is_dir())
            .map(|p| p.to_path_buf())
            // fallback: use the configured base dir stripped of Sources/<Module>
            .unwrap_or_else(|| {
                // base_dir is e.g. "packages/swift/Sources/LiterLlm"
                // go up two levels to get "packages/swift"
                PathBuf::from(&base_dir)
                    .parent()
                    .and_then(|p| p.parent())
                    .map(|p| p.to_path_buf())
                    .unwrap_or_else(|| PathBuf::from("packages/swift"))
            });
        if let Some(bridge_files) = emit_swift_bridge_files(&api.crate_name, &binding_crate_name, &package_root)? {
            files.extend(bridge_files);
        }

        // Emit Swift{Trait}Box.swift into Sources/RustBridge/ for every OptionsField bridge.
        // This class is referenced by the @_cdecl shims generated into that same directory
        // by swift-bridge, so it MUST live in the RustBridge module — not HtmlToMarkdown.
        let rust_bridge_sources = package_root.join("Sources").join("RustBridge");
        for box_file in emit_inbound_box_files(api, config, &rust_bridge_sources) {
            files.push(box_file);
        }

        // Emit trait bridge protocol and adapter files for outbound plugins.
        // Swift{Trait}Bridge.swift files are emitted into Sources/<Module>/ alongside the main binding file.
        let trait_bridge_configs: Vec<(String, &TraitBridgeConfig, &TypeDef)> = config
            .trait_bridges
            .iter()
            .filter_map(|b| {
                api.types
                    .iter()
                    .find(|t| t.is_trait && t.name == b.trait_name)
                    .map(|t| (b.trait_name.clone(), b, t))
            })
            .collect();

        let module_dir = if config.explicit_output.swift.is_some() {
            base_path.clone()
        } else {
            base_path.join("Sources").join(&module_name)
        };

        for (filename, content) in trait_bridge::gen_trait_bridge_files(&trait_bridge_configs, &exclude_types) {
            let path = module_dir.join(&filename);
            files.push(GeneratedFile {
                path,
                content,
                generated_header: false,
            });
        }

        Ok(files)
    }

    fn build_config(&self) -> Option<BuildConfig> {
        Some(BuildConfig {
            tool: "swift",
            crate_suffix: "-swift",
            build_dep: BuildDependency::None,
            // Build the Rust bridge crate first so swift-bridge codegen produces
            // the Swift glue files that the Swift Package consumes.
            post_build: vec![PostBuildStep::RunCommand {
                cmd: "cargo",
                args: vec!["build", "--release"],
            }],
        })
    }
}

fn can_emit_first_class_struct(
    ty: &crate::core::ir::TypeDef,
    _mapper: &SwiftMapper,
    _exclude_fields: &std::collections::HashSet<String>,
    known_dto_names: &std::collections::HashSet<String>,
) -> bool {
    !ty.is_opaque
        && ty.has_serde
        && !ty.fields.is_empty()
        && binding_fields(&ty.fields).all(|field| first_class_field_supported(&field.ty, known_dto_names))
    // Note: we no longer require emit_into_rust_direct_call to succeed.  Types whose
    // fields include Vec<Named>, Named, or Map fall back to the JSON roundtrip path in
    // intoRust() automatically (the same `{type_snake}_from_json` shim is emitted by
    // the Rust crate side for all types where has_constructor_extern returns false).
    // The exclude_fields parameter is retained in the signature for the intoRust emitter.
    && !ty.fields.iter().all(|f| f.binding_excluded)
}

/// Returns `true` when a field type can be represented as a stored property in a first-class
/// Swift struct and marshaled through the RustBridge FFI layer.
///
/// Accepted:
/// - Primitives and Bool
/// - String
/// - Named(S) where S is a known DTO or enum in the same API
/// - Vec<T> where T is itself accepted (Vec<Primitive>, Vec<String>, Vec<Named(S)>)
/// - Optional<T> (via TypeRef::Optional) where T is accepted — covers Optional<String>,
///   Optional<Primitive>, Optional<Named>, Optional<Vec<T>>
/// - field.optional = true is handled at the call site; this function only sees the TypeRef
///
/// Rejected (fall through to typealias):
/// - Map<K, V> — bridge layer serialises maps to JSON String; per-field JSON decode is
///   complex and rarely worth the ergonomic gain vs the typealias
/// - Path, Bytes, Duration, Char, Json — not representable as idiomatic Swift stored props
///   without additional infra
fn first_class_field_supported(ty: &TypeRef, known_dto_names: &std::collections::HashSet<String>) -> bool {
    match ty {
        TypeRef::Primitive(_) | TypeRef::String => true,
        TypeRef::Named(name) => known_dto_names.contains(name),
        TypeRef::Vec(inner) => first_class_field_supported(inner, known_dto_names),
        TypeRef::Optional(inner) => first_class_field_supported(inner, known_dto_names),
        // Map, Path, Bytes, Duration, Char, Json, Unit — not yet supported
        _ => false,
    }
}

/// Emits a first-class Swift struct (`public struct Foo: Codable, Sendable, Hashable`)
/// for a non-opaque DTO that has serde derives and at least one field.
///
/// Layer 1: public struct declaration with `public let` stored properties,
///          memberwise `public init` with default-nil optionals, and `CodingKeys`
///          when field names differ from their wire (serde snake_case) keys.
/// Layer 2: `internal extension Foo` with `init(_ rb: RustBridge.Foo) throws` and
///          `func intoRust() throws -> RustBridge.Foo` for FFI marshaling.
fn emit_first_class_struct(
    ty: &crate::core::ir::TypeDef,
    mapper: &SwiftMapper,
    exclude_fields: &std::collections::HashSet<String>,
    known_dto_names: &std::collections::HashSet<String>,
    unit_enum_names: &std::collections::HashSet<String>,
    untagged_enum_names: &std::collections::HashSet<String>,
    out: &mut String,
) {
    use crate::codegen::type_mapper::TypeMapper;
    use crate::core::ir::TypeRef;
    use heck::ToLowerCamelCase;

    let type_name = &ty.name;
    let type_snake = AsSnakeCase(type_name.as_str()).to_string();

    // ── Layer 1: public struct declaration ───────────────────────────────────
    out.push_str(&format!("public struct {type_name}: Codable, Sendable, Hashable {{\n"));

    // Emit `public let` stored properties.
    // The extractor unwraps Option<T> into (ty: T, optional: true) -- check field.optional
    // in addition to TypeRef::Optional to handle both IR representations correctly.
    //
    // Note: `swift_case_ident` is used here (and at every other site that emits a
    // Swift-side identifier) so that fields named after Swift reserved keywords
    // are wrapped in backticks (`` `default` ``) rather than escaped with a
    // trailing underscore.
    let visible_fields: Vec<_> = binding_fields(&ty.fields).collect();
    for field in &visible_fields {
        emit_doc_comment(&field.doc, "    ", out);
        let camel = swift_case_ident(&field.name.to_lower_camel_case());
        let already_optional = matches!(&field.ty, TypeRef::Optional(_));
        let swift_ty = mapper.map_type(&field.ty);
        if field.optional && !already_optional {
            out.push_str(&format!("    public let {camel}: {swift_ty}?\n"));
        } else {
            out.push_str(&format!("    public let {camel}: {swift_ty}\n"));
        }
    }

    // Memberwise init with default-nil for Optional fields.
    let params: Vec<String> = visible_fields
        .iter()
        .map(|field| {
            let camel = swift_case_ident(&field.name.to_lower_camel_case());
            let already_optional = matches!(&field.ty, TypeRef::Optional(_));
            let swift_ty = mapper.map_type(&field.ty);
            if field.optional && !already_optional {
                format!("{camel}: {swift_ty}? = nil")
            } else if already_optional {
                format!("{camel}: {swift_ty} = nil")
            } else {
                format!("{camel}: {swift_ty}")
            }
        })
        .collect();
    out.push_str(&format!("    public init({}) {{\n", params.join(", ")));
    for field in &visible_fields {
        let camel = swift_case_ident(&field.name.to_lower_camel_case());
        out.push_str(&format!("        self.{camel} = {camel}\n"));
    }
    out.push_str("    }\n");

    // CodingKeys: emit when at least one field's camelCase name differs from the
    // serde wire key, OR when `ty.has_default` is true (the custom decoder below
    // references `CodingKeys.<camel>` for every field).
    let needs_coding_keys = ty.has_default
        || visible_fields.iter().any(|field| {
            let camel = field.name.to_lower_camel_case();
            camel != field.name
        });
    if needs_coding_keys {
        out.push_str("    private enum CodingKeys: String, CodingKey {\n");
        for field in &visible_fields {
            let camel = swift_case_ident(&field.name.to_lower_camel_case());
            let wire_key = &field.name;
            out.push_str(&format!("        case {camel} = \"{wire_key}\"\n"));
        }
        out.push_str("    }\n");
    }

    // Custom `init(from decoder:)` when the Rust source has `#[derive(Default)]` or
    // a manual `impl Default`. Swift's auto-synthesized Codable decoder rejects JSON
    // that omits non-Optional declared properties; the custom init uses
    // `decodeIfPresent + ?? <fallback>` so JSON inputs from Rust serializers using
    // `#[serde(default)]` / `#[serde(skip_serializing_if = ...)]` decode successfully.
    if ty.has_default {
        emit_decoder_init(mapper, &visible_fields, out);
    }

    out.push_str("}\n");

    // ── Layer 2: internal FFI conversion extension ────────────────────────────
    out.push_str(&format!("\n// MARK: - Internal FFI conversions for {type_name}\n"));
    out.push_str(&format!("internal extension {type_name} {{\n"));

    // init(_ rb: RustBridge.FooRef) throws
    //
    // Accept the `Ref` base class rather than the owned class so callers can
    // pass either: an owned `RustBridge.Foo` (which is-a `RustBridge.FooRef`
    // via swift-bridge's class hierarchy `Foo: FooRefMut: FooRef`) or a
    // `FooRef` borrowed out of a `RustVec<Foo>` iteration. Without this, code
    // like `try rb.layouts().map { try Layout($0) }` fails to compile with
    // `cannot convert value of type 'LayoutRef' to expected argument type 'Layout'`.
    //
    // The Swift-side property name uses backtick escape (`swift_case_ident`),
    // while the RustBridge accessor uses the trailing-underscore form
    // (`swift_ident`) to match the swift-bridge-generated Rust method name —
    // see `gen_rust_crate::extern_block::emit_extern_block_for_type`.
    out.push_str(&format!("    init(_ rb: RustBridge.{type_name}Ref) throws {{\n"));
    for field in &visible_fields {
        let swift_field = swift_case_ident(&field.name.to_lower_camel_case());
        // The RustBridge accessor name now follows lowerCamelCase because the
        // swift-bridge getter extern emits `swift_name = "<camel>"` (see
        // `gen_rust_crate::extern_block::emit_extern_block_for_type`).
        let rust_accessor = swift_ident(&field.name.to_lower_camel_case());
        let is_optional = field.optional || matches!(&field.ty, TypeRef::Optional(_));

        // If the wrapper-side getter is unbridgeable (skipped by
        // `is_unbridgeable_getter`), the Rust crate emits no accessor at all —
        // calling `rb.{field}()` would fail to compile. Fall back to a sensible
        // default: nil for optional fields, JSONDecoder roundtrip of an empty
        // value for required ones (the public struct still surfaces the field
        // for direct construction / JSON decode paths).
        let expr = if is_field_unbridgeable_for_init(ty, field, exclude_fields, known_dto_names) {
            if is_optional {
                "nil".to_string()
            } else {
                // Fields here are non-optional and unbridgeable — extraordinarily rare
                // in practice. Emit a JSONDecoder-of-default placeholder so the file
                // compiles; the JSON path on `intoRust()` covers the round-trip.
                let swift_ty = mapper.map_type(&field.ty);
                format!("try JSONDecoder().decode({swift_ty}.self, from: Data(\"null\".utf8))")
            }
        } else if is_untagged_enum_type(&field.ty, untagged_enum_names) {
            // Untagged-enum field (`#[serde(untagged)]`): the swift-bridge accessor returns a
            // `RustString` containing the JSON-serialized value rather than an opaque
            // `RustBridge.{Name}Ref`. There is no `init(_ rb: RustBridge.{Name}Ref)` for
            // untagged enums, so calling `try {Name}(rb.{field}())` produces a compile error
            // ("missing argument label 'from:'" / "RustString does not conform to Decoder").
            // Decode via JSONDecoder from the JSON payload instead.
            let swift_ty = mapper.map_type(&field.ty);
            let swift_ty_with_opt = if is_optional && !matches!(&field.ty, TypeRef::Optional(_)) {
                format!("{swift_ty}?")
            } else {
                swift_ty
            };
            format!(
                "try JSONDecoder().decode({swift_ty_with_opt}.self, from: \
                 (rb.{rust_accessor}().toString().data(using: .utf8) ?? Data(\"null\".utf8)))"
            )
        } else if needs_json_bridge_for_swift(&field.ty) {
            // Field is bridged as a JSON string at the Rust boundary — the getter
            // returns a non-optional `RustString` whose contents are the JSON-
            // encoded value (`"null"` when the source field was None). Decode
            // through JSONDecoder so the Swift property receives the typed value.
            let swift_ty = mapper.map_type(&field.ty);
            let swift_ty_with_opt = if is_optional && !matches!(&field.ty, TypeRef::Optional(_)) {
                format!("{swift_ty}?")
            } else {
                swift_ty
            };
            format!(
                "try JSONDecoder().decode({swift_ty_with_opt}.self, from: \
                 (rb.{rust_accessor}().toString().data(using: .utf8) ?? Data(\"null\".utf8)))"
            )
        } else {
            swift_ffi_read_expr(
                &field.ty,
                is_optional,
                &rust_accessor,
                known_dto_names,
                unit_enum_names,
                untagged_enum_names,
            )
        };
        out.push_str(&format!("        self.{swift_field} = {expr}\n"));
    }
    out.push_str("    }\n");

    // func intoRust() — prefer a direct `RustBridge.{Type}(...)` bulk-constructor call
    // when the swift-bridge `#[swift_bridge(init)] fn new(...)` extern is emitted for
    // this type AND every constructor field can be converted to its bridge argument
    // without a JSON detour. Falls back to the JSON roundtrip via the `{type}_from_json`
    // shim otherwise (e.g. fields needing JSON bridge, types without a Default impl).
    //
    // The bulk path mirrors the symmetric direct-field-access pattern in
    // `init(_ rb: RustBridge.{Type}) throws` above and avoids the JSONEncoder + Rust-side
    // `serde_json::from_str` work for primitive-only DTOs.
    out.push_str(&format!("    func intoRust() throws -> RustBridge.{type_name} {{\n"));
    let direct_call = emit_into_rust_direct_call(ty, mapper, exclude_fields, type_name);
    match direct_call {
        Some(call) => out.push_str(&call),
        None => {
            let from_json_fn = format!("{type_snake}_from_json").to_lower_camel_case();
            out.push_str("        let data = try JSONEncoder().encode(self)\n");
            out.push_str("        let json = String(data: data, encoding: .utf8) ?? \"{}\"\n");
            out.push_str(&format!("        return try RustBridge.{from_json_fn}(json)\n"));
        }
    }
    out.push_str("    }\n");

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

/// Renders a typed `DefaultValue` into a Swift literal expression suitable for use as
/// a `??` fallback. Returns `None` for variants that have no direct Swift literal
/// (`Empty`, `None`, `EnumVariant`), so callers fall back to a type-based default.
///
/// `FloatLiteral` values that are NaN or infinite are also rejected so generated code
/// stays parseable — callers handle those by falling back to a type-based default.
fn swift_typed_default_literal(dv: &DefaultValue) -> Option<String> {
    match dv {
        DefaultValue::BoolLiteral(true) => Some("true".to_string()),
        DefaultValue::BoolLiteral(false) => Some("false".to_string()),
        DefaultValue::IntLiteral(n) => Some(n.to_string()),
        DefaultValue::FloatLiteral(f) => {
            if f.is_nan() || f.is_infinite() {
                None
            } else {
                // Emit at least one decimal so the literal parses as a Swift
                // floating-point literal rather than an integer literal.
                let s = if f.fract() == 0.0 {
                    format!("{f:.1}")
                } else {
                    f.to_string()
                };
                Some(s)
            }
        }
        DefaultValue::StringLiteral(s) => {
            // Conservative escape for Swift string literal: backslash, double quote,
            // newline, carriage return, tab. Anything else passes through.
            let mut escaped = String::with_capacity(s.len() + 2);
            escaped.push('"');
            for ch in s.chars() {
                match ch {
                    '\\' => escaped.push_str("\\\\"),
                    '"' => escaped.push_str("\\\""),
                    '\n' => escaped.push_str("\\n"),
                    '\r' => escaped.push_str("\\r"),
                    '\t' => escaped.push_str("\\t"),
                    c => escaped.push(c),
                }
            }
            escaped.push('"');
            Some(escaped)
        }
        // EnumVariant requires knowledge of the target enum's Swift case name and is
        // not safely renderable from the variant string alone — fall back to a plain
        // `decode(T.self, ...)` (no `??`).
        DefaultValue::EnumVariant(_) => None,
        DefaultValue::Empty | DefaultValue::None => None,
    }
}

/// Returns a Swift literal expression for the "natural" default of a `TypeRef`, used
/// as a fallback when the field has `typed_default = Empty/None` or no typed default
/// at all.
///
/// Returns `None` for `Named(_)`, `Bytes`, `Path`, `Duration`, `Json`, `Char`, `Unit` —
/// the caller then emits a plain `decode(T.self, ...)` which relies on the nested
/// type's own decoder.
fn swift_type_based_default(ty: &TypeRef) -> Option<String> {
    match ty {
        TypeRef::Primitive(prim) => match prim {
            PrimitiveType::Bool => Some("false".to_string()),
            PrimitiveType::U8
            | PrimitiveType::I8
            | PrimitiveType::U16
            | PrimitiveType::I16
            | PrimitiveType::U32
            | PrimitiveType::I32
            | PrimitiveType::U64
            | PrimitiveType::I64
            | PrimitiveType::Usize
            | PrimitiveType::Isize => Some("0".to_string()),
            PrimitiveType::F32 | PrimitiveType::F64 => Some("0".to_string()),
        },
        TypeRef::String => Some("\"\"".to_string()),
        TypeRef::Vec(_) => Some("[]".to_string()),
        TypeRef::Map(_, _) => Some("[:]".to_string()),
        TypeRef::Optional(_) => Some("nil".to_string()),
        // Named: cannot synthesize a default value without knowing the target type.
        // Bytes/Path/Duration/Json/Char/Unit are out of scope — caller falls through
        // to a plain decode.
        _ => None,
    }
}

/// Emits a custom `public init(from decoder: any Decoder) throws` body that uses
/// `decodeIfPresent + ?? <fallback>` for every non-Optional field with a known
/// default, `decodeIfPresent ?? nil` for Optional fields, and plain
/// `decode(T.self, ...)` for non-Optional fields with no safe Swift fallback
/// (e.g. nested `Named` structs).
fn emit_decoder_init(mapper: &SwiftMapper, visible_fields: &[&crate::core::ir::FieldDef], out: &mut String) {
    out.push_str("    public init(from decoder: any Decoder) throws {\n");
    out.push_str("        let container = try decoder.container(keyedBy: CodingKeys.self)\n");
    for field in visible_fields {
        let camel = swift_case_ident(&field.name.to_lower_camel_case());
        let already_optional = matches!(&field.ty, TypeRef::Optional(_));
        let is_optional = field.optional || already_optional;
        let swift_ty = mapper.map_type(&field.ty);

        if is_optional {
            // Optional fields decode to nil when the key is missing OR when
            // present-and-null. Strip a trailing `?` so the type passed to
            // `decodeIfPresent` is the inner (non-optional) form —
            // `decodeIfPresent(T.self, ...)` already returns `T?`.
            let inner_ty = swift_ty.strip_suffix('?').unwrap_or(&swift_ty);
            out.push_str(&format!(
                "        self.{camel} = try container.decodeIfPresent({inner_ty}.self, forKey: .{camel}) ?? nil\n"
            ));
            continue;
        }

        // Non-Optional field: pick a fallback literal in this priority order:
        //   1. Typed default literal (BoolLiteral / IntLiteral / FloatLiteral /
        //      StringLiteral).
        //   2. Type-based default for collections / primitives / strings:
        //      `[]`, `[:]`, `false`, `0`, `""`.
        //   3. None → emit plain `decode(T.self, ...)` with no fallback.
        let fallback = field
            .typed_default
            .as_ref()
            .and_then(swift_typed_default_literal)
            .or_else(|| swift_type_based_default(&field.ty));

        match fallback {
            Some(fb) => {
                out.push_str(&format!(
                    "        self.{camel} = try container.decodeIfPresent({swift_ty}.self, forKey: .{camel}) ?? {fb}\n"
                ));
            }
            None => {
                out.push_str(&format!(
                    "        self.{camel} = try container.decode({swift_ty}.self, forKey: .{camel})\n"
                ));
            }
        }
    }
    out.push_str("    }\n");
}

/// Returns the Swift body of `intoRust()` as a direct `RustBridge.{Type}(...)` call when
/// every constructor field can be converted without JSON, or `None` when at least one
/// field requires the JSON fallback (e.g. JSON-bridged Map/Json fields, types without a
/// Default impl that prevent the bulk constructor extern from being emitted).
///
/// Conversions supported in this PoC:
/// - Primitive (Int/UInt/Bool/Float/Double): pass `self.{camel}` directly.
/// - String: pass `self.{camel}` (Swift `String` conforms to swift-bridge's `IntoRustString`).
/// - Named(struct): recurse via `try self.{camel}.intoRust()`.
/// - Vec<Primitive | String | Named>: build a `RustVec<T>` and push each converted element.
/// - Optional<Primitive | String>: pass `self.{camel}` (swift-bridge handles native nullable).
///
/// Returns `None` for anything else (Map, Json, Path, Bytes, Duration, Char, Optional<Vec>,
/// Optional<Named>, Vec<Vec<...>>) so the caller can use the JSON fallback.
fn emit_into_rust_direct_call(
    ty: &crate::core::ir::TypeDef,
    _mapper: &SwiftMapper,
    exclude_fields: &std::collections::HashSet<String>,
    type_name: &str,
) -> Option<String> {
    use crate::backends::swift::gen_rust_crate::extern_block::{constructor_fields, has_constructor_extern};
    use heck::ToLowerCamelCase;

    if !has_constructor_extern(ty, exclude_fields) {
        return None;
    }

    let ctor_fields = constructor_fields(ty, exclude_fields);
    // If any visible (binding-visible) field is dropped from the constructor — e.g.
    // listed in `[crates.<crate>.swift] exclude_fields` — the bulk constructor would
    // silently default that field while the JSON roundtrip preserves whatever serde
    // encoded. Fall back to JSON in that case to avoid a behaviour change.
    let visible_count = ty.fields.iter().filter(|f| !f.binding_excluded).count();
    if ctor_fields.len() != visible_count {
        return None;
    }

    // Build per-field conversion expressions. Bail to None (JSON fallback) on the first
    // field type we don't yet support.
    let mut prelude = String::new();
    let mut args: Vec<String> = Vec::with_capacity(ctor_fields.len());
    for field in &ctor_fields {
        let camel = swift_case_ident(&field.name.to_lower_camel_case());
        let local = format!("__{}", field.name.to_lower_camel_case());
        match field_intorust_arg(&field.ty, field.optional, &camel, &local) {
            Some(FieldArg::Direct(expr)) => args.push(expr),
            Some(FieldArg::WithPrelude { prelude: p, arg }) => {
                prelude.push_str(&p);
                args.push(arg);
            }
            None => return None,
        }
    }

    // Emit: `return RustBridge.{TypeName}(arg1, arg2, ...)` — swift-bridge's
    // convenience init takes positional `_` parameters in field-declaration order.
    let mut body = String::new();
    body.push_str(&prelude);
    body.push_str(&format!(
        "        return RustBridge.{type_name}({args})\n",
        args = args.join(", ")
    ));
    Some(body)
}

/// One field's contribution to the `RustBridge.{Type}(...)` argument list.
enum FieldArg {
    /// Inline expression — no temporaries needed (`self.foo`, etc.).
    Direct(String),
    /// Multi-statement prelude that materialises a temporary, plus the argument
    /// referencing that temporary (`__foo`).
    WithPrelude { prelude: String, arg: String },
}

/// Translate a single field into a `RustBridge.{Type}(...)` argument.
///
/// Returns `None` when the field's Swift type cannot yet be passed directly to the
/// swift-bridge convenience init (Map, Json, Path, Bytes, Duration, Char, etc.) — the
/// caller falls back to the JSON roundtrip.
fn field_intorust_arg(
    ty: &crate::core::ir::TypeRef,
    field_optional: bool,
    self_property: &str,
    local: &str,
) -> Option<FieldArg> {
    use crate::core::ir::TypeRef;

    // Unwrap Optional(T) and merge with field.optional — both encode "nullable" in
    // different parts of the IR.
    let (inner_ty, is_optional) = match ty {
        TypeRef::Optional(inner) => (inner.as_ref(), true),
        _ => (ty, field_optional),
    };

    match inner_ty {
        // Primitives and bools pass straight through — Swift Int/UInt/Bool/Float/Double
        // map directly onto swift-bridge's bridged Rust primitives, including the
        // `Optional<T>` variants.
        TypeRef::Primitive(_) => Some(FieldArg::Direct(format!("self.{self_property}"))),

        // String fields must be wrapped with `RustString(...)`. swift-bridge 0.1.59 emits
        // the convenience init using a *single* `GenericIntoRustString: IntoRustString`
        // type parameter shared across every String-like argument in the signature
        // (including `RustVec<GenericIntoRustString>` for `Vec<String>` fields). When any
        // field forces `RustString` (via the Vec path in `emit_vec_arg`), every other
        // String arg must also be `RustString` — Swift `String` cannot unify with
        // `RustString` under the shared generic. Wrapping unconditionally keeps the
        // init linkable regardless of which other fields appear.
        // Optional `String?` -> `self.foo.map(RustString.init)` — preserves nil.
        TypeRef::String if !is_optional => Some(FieldArg::Direct(format!("RustString(self.{self_property})"))),
        TypeRef::String => Some(FieldArg::Direct(format!("self.{self_property}.map(RustString.init)"))),

        // Nested struct field: recurse via the symmetric `intoRust()` method on the
        // first-class struct. swift-bridge takes the wrapper class (e.g. `RustBridge.Span`).
        TypeRef::Named(_) if !is_optional => Some(FieldArg::Direct(format!("try self.{self_property}.intoRust()"))),

        // Vec<T>: build a `RustVec<U>` and push each converted element. The element
        // converter is the inner type's own intoRust handling.
        TypeRef::Vec(elem) if !is_optional => emit_vec_arg(elem, self_property, local),

        // Other forms (Map, Path, Bytes, Duration, Char, Json, Optional<Vec>,
        // Optional<Named>, Vec<Vec<…>>) take the JSON fallback for now.
        _ => None,
    }
}

/// Emit a `RustVec<U>` materialisation prelude and the argument referencing it.
///
/// Supports inner types: Primitive (`RustVec<Int>` etc.), String (`RustVec<RustString>`,
/// because swift-bridge's `Vectorizable` is implemented for `RustString` not `String`),
/// and Named (`RustVec<RustBridge.Foo>` via per-element `intoRust()`).
fn emit_vec_arg(elem: &crate::core::ir::TypeRef, self_property: &str, local: &str) -> Option<FieldArg> {
    use crate::core::ir::TypeRef;

    let (rust_vec_param, elem_expr): (String, String) = match elem {
        TypeRef::Primitive(prim) => {
            let swift_prim = SwiftMapper.primitive(prim).into_owned();
            (swift_prim, "__elem".to_string())
        }
        // swift-bridge: `RustVec<T>` requires `T: Vectorizable`. Only `RustString` satisfies
        // that for textual data — bare Swift `String` does not. Wrap each element with
        // `RustString(__elem)` so the resulting `RustVec<RustString>` matches the Rust
        // `Vec<String>` ABI expected by the bridge.
        TypeRef::String => ("RustString".to_string(), "RustString(__elem)".to_string()),
        TypeRef::Named(name) => (format!("RustBridge.{name}"), "try __elem.intoRust()".to_string()),
        _ => return None,
    };

    // RustVec.push(value:) consumes one element at a time. The literal Swift array
    // (`self.{prop}`) iterates fine in a `for`-loop. The Rust-side extern takes
    // ownership of the resulting RustVec, so we don't need any extra cleanup.
    let prelude = format!(
        "        let {local} = RustVec<{rust_vec_param}>()\n        \
         for __elem in self.{self_property} {{ {local}.push(value: {elem_expr}) }}\n",
    );
    Some(FieldArg::WithPrelude {
        prelude,
        arg: local.to_string(),
    })
}

/// Returns the Swift expression to read a field value from a `RustBridge` accessor call.
///
/// swift-bridge exposes Rust struct fields as method calls: `.field_name()`.
///
/// Type-specific conversions:
/// - `String` → `.toString()` (RustString → Swift String)
/// - `Named(S)` → `try S(rb.field())` (RustBridge.S → first-class Swift struct via init)
/// - `Vec<String>` → `rb.field().map { $0.toString() }` (RustVec<RustString> → [String])
/// - `Vec<Primitive>` → `Array(rb.field())` (RustVec<T> → [T], RustVec is a Collection)
/// - `Vec<Named(S)>` → `try rb.field().map { try S($0) }` (RustVec<RustBridge.S> → [S])
/// - Optional variants use `?` chains accordingly.
///
/// `field_optional` is true when `field.optional == true` (extractor-unwrapped IR form).
/// `TypeRef::Optional(inner)` is the TypeRef-wrapped form — both are handled.
fn swift_ffi_read_expr(
    ty: &crate::core::ir::TypeRef,
    field_optional: bool,
    accessor: &str,
    known_dto_names: &std::collections::HashSet<String>,
    unit_enum_names: &std::collections::HashSet<String>,
    untagged_enum_names: &std::collections::HashSet<String>,
) -> String {
    use crate::core::ir::TypeRef;

    // When the field is optional in the extractor-unwrapped IR form (field.optional == true
    // but TypeRef is NOT Optional), the swift-bridge getter returns `T?` natively.
    // We compose the same `?`-chain logic as for TypeRef::Optional.
    let opt = field_optional && !matches!(ty, TypeRef::Optional(_));

    match ty {
        // String fields: getter returns RustString; call .toString().
        TypeRef::String if opt => format!("rb.{accessor}()?.toString()"),
        TypeRef::String => format!("rb.{accessor}().toString()"),

        // Named(S) where S is a unit serde enum: getter returns String (serde-serialized).
        // Convert via raw value init (`:String, Codable` enum).
        // When optional: `rb.field().flatMap { S(rawValue: $0.toString()) }`
        // The `flatMap` is called on the bridge `Optional<RustString>` directly so
        // Swift picks `Optional.flatMap` instead of `Sequence.flatMap` (String is a
        // Sequence with `Character` elements — chaining `?.toString().flatMap`
        // would dispatch to the Sequence variant and break compilation).
        // When required: `S(rawValue: rb.field().toString())` (force-safe: serde guarantees valid value)
        TypeRef::Named(name) if unit_enum_names.contains(name) && opt => {
            format!("rb.{accessor}().flatMap {{ {name}(rawValue: $0.toString()) }}")
        }
        TypeRef::Named(name) if unit_enum_names.contains(name) => {
            // The Rust getter for enum fields returns `String` (the serde-serialized name).
            // Since the bridge generates the value, the rawValue is always valid for known variants
            // (unknown variants map to `other` when the enum has a catch-all, or panic otherwise).
            // Using non-optional init here: callers that care about safety should make the field optional.
            format!(
                "{name}(rawValue: rb.{accessor}().toString()) ?? {{ fatalError(\"Unknown {name}: \\(rb.{accessor}().toString())\") }}()"
            )
        }

        // Named(S) where S is a first-class struct: getter returns RustBridge.S (or S?).
        // Convert with the symmetric `init(_ rb:) throws` on the first-class struct.
        // Note: untagged enums are excluded here because they are in `known_dto_names` but
        // bridge as RustString — those cases are handled at the call site before reaching
        // this function (via `is_untagged_enum_type` check in the field init loop).
        TypeRef::Named(name) if known_dto_names.contains(name) && !untagged_enum_names.contains(name) && opt => {
            format!("try rb.{accessor}().map {{ try {name}($0) }}")
        }
        TypeRef::Named(name) if known_dto_names.contains(name) && !untagged_enum_names.contains(name) => {
            format!("try {name}(rb.{accessor}())")
        }

        // Vec<T>: getter returns RustVec<T> (a Swift Collection/Sequence).
        TypeRef::Vec(inner) if opt => {
            // The getter return type for optional Vec is RustVec<T>? when the field is
            // declared optional via `field.optional` (not TypeRef::Optional).
            // Use `?.map` for the optional chain.
            match inner.as_ref() {
                TypeRef::Primitive(_) => format!("rb.{accessor}().map {{ Array($0) }}"),
                TypeRef::String => format!("rb.{accessor}()?.map {{ $0.as_str().toString() }}"),
                // Vec<UntaggedEnum>: each element is a JSON-encoded RustString.
                // Decode each element via JSONDecoder in the map closure.
                TypeRef::Named(name) if untagged_enum_names.contains(name) => {
                    format!(
                        "try rb.{accessor}()?.map {{ (s: RustStringRef) -> {name} in \
                         let d = s.as_str().toString().data(using: .utf8) ?? Data(); \
                         return try JSONDecoder().decode({name}.self, from: d) }}"
                    )
                }
                TypeRef::Named(name) if known_dto_names.contains(name) => {
                    format!("try rb.{accessor}()?.map {{ try {name}($0) }}")
                }
                _ => {
                    let map_expr = vec_elem_convert_expr(inner, known_dto_names);
                    format!("rb.{accessor}()?.map {{ {map_expr} }}")
                }
            }
        }
        TypeRef::Vec(inner) => match inner.as_ref() {
            TypeRef::Primitive(_) => format!("Array(rb.{accessor}())"),
            TypeRef::String => format!("rb.{accessor}().map {{ $0.as_str().toString() }}"),
            // Vec<UntaggedEnum>: each element is a JSON-encoded RustString.
            // Decode each element via JSONDecoder in the map closure.
            TypeRef::Named(name) if untagged_enum_names.contains(name) => {
                format!(
                    "try rb.{accessor}().map {{ (s: RustStringRef) -> {name} in \
                     let d = s.as_str().toString().data(using: .utf8) ?? Data(); \
                     return try JSONDecoder().decode({name}.self, from: d) }}"
                )
            }
            TypeRef::Named(name) if known_dto_names.contains(name) => {
                format!("try rb.{accessor}().map {{ try {name}($0) }}")
            }
            _ => format!("rb.{accessor}()"),
        },

        // TypeRef::Optional(inner) — the extractor-wrapped nullable form.
        // For Optional<Named(unit_enum)>: getter returns Option<String> (serde-serialized).
        // Note: Optional<Named(untagged_enum)> is handled at the call site via
        // `is_untagged_enum_type` before reaching this function.
        TypeRef::Optional(inner) => match inner.as_ref() {
            TypeRef::String => format!("rb.{accessor}()?.toString()"),
            TypeRef::Named(name) if unit_enum_names.contains(name) => {
                format!("rb.{accessor}().flatMap {{ {name}(rawValue: $0.toString()) }}")
            }
            TypeRef::Named(name) if known_dto_names.contains(name) && !untagged_enum_names.contains(name) => {
                format!("try rb.{accessor}().map {{ try {name}($0) }}")
            }
            TypeRef::Vec(elem) => match elem.as_ref() {
                TypeRef::String => format!("rb.{accessor}()?.map {{ $0.as_str().toString() }}"),
                TypeRef::Named(name) if known_dto_names.contains(name) && !untagged_enum_names.contains(name) => {
                    format!("try rb.{accessor}()?.map {{ try {name}($0) }}")
                }
                TypeRef::Primitive(_) => format!("rb.{accessor}().map {{ Array($0) }}"),
                _ => format!("rb.{accessor}()"),
            },
            _ => format!("rb.{accessor}()"),
        },

        // Primitives, and anything else the bridge passes through directly.
        _ => format!("rb.{accessor}()"),
    }
}

/// Returns the element-level Swift expression used in a `.map { ... }` closure when
/// converting a `RustVec<T>` element to its first-class Swift equivalent.
fn vec_elem_convert_expr(
    inner: &crate::core::ir::TypeRef,
    known_dto_names: &std::collections::HashSet<String>,
) -> String {
    use crate::core::ir::TypeRef;
    match inner {
        // RustVec<RustString> iteration yields RustStringRef — see RustStringRef
        // shim comment in `forwarder_return_conversion_suffix_inner` for why we use
        // `as_str().toString()` instead of `toString()` directly.
        TypeRef::String => "$0.as_str().toString()".to_string(),
        TypeRef::Named(name) if known_dto_names.contains(name) => format!("try {name}($0)"),
        TypeRef::Primitive(_) => "$0".to_string(),
        _ => "$0".to_string(),
    }
}

/// Emits a custom Codable conformance for a serde-internally-tagged enum.
/// Handles variant-tag decoding/encoding and respects field renames.
fn emit_serde_tagged_codable(en: &EnumDef, out: &mut String, mapper: &SwiftMapper) {
    let tag_key = en.serde_tag.as_deref().unwrap_or("type");

    // Emit CodingKeys enum
    out.push_str("    private enum CodingKeys: String, CodingKey {\n");
    out.push_str(&format!("        case {}\n", tag_key));

    // Collect all unique field names across all variants. Use swift_associated_label
    // so positional tuple-variant fields (named "0", "1", … or "_0", "_1", …) become
    // `field0`, `field1`, … — bare digits are invalid Swift identifiers, and the
    // init(from:)/encode(to:) bodies below already use the label-synthesized form
    // via swift_associated_label, so the CodingKeys cases must match.
    let mut field_keys = std::collections::BTreeSet::new();
    for variant in &en.variants {
        for (idx, field) in variant.fields.iter().enumerate() {
            let swift_name = swift_associated_label(&field.name, idx);
            let rust_name = field.serde_rename.as_deref().unwrap_or(&field.name);
            field_keys.insert((swift_name, rust_name.to_string()));
        }
    }

    for (swift_name, rust_name) in field_keys {
        if swift_name == rust_name {
            out.push_str(&format!("        case {}\n", swift_name));
        } else {
            out.push_str(&format!("        case {} = \"{}\"\n", swift_name, rust_name));
        }
    }

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

    // Emit init(from:)
    out.push_str("    public init(from decoder: Decoder) throws {\n");
    out.push_str("        let container = try decoder.container(keyedBy: CodingKeys.self)\n");
    out.push_str(&format!(
        "        let type = try container.decode(String.self, forKey: .{})\n",
        tag_key
    ));
    out.push_str("        switch type {\n");

    for variant in &en.variants {
        // Compute the variant tag value based on serde_rename_all and serde_rename
        let variant_tag = if let Some(rename) = &variant.serde_rename {
            rename.clone()
        } else {
            match en.serde_rename_all.as_deref() {
                Some("camelCase") => variant.name.to_lower_camel_case(),
                Some("snake_case") => variant.name.to_snake_case(),
                Some("SCREAMING_SNAKE_CASE") => variant.name.to_snake_case().to_uppercase(),
                Some("PascalCase") => variant.name.to_upper_camel_case(),
                Some("kebab-case") => variant.name.to_kebab_case(),
                Some("lowercase") => variant.name.to_lowercase(),
                Some("UPPERCASE") => variant.name.to_uppercase(),
                _ => variant.name.clone(),
            }
        };

        let case_name = swift_case_ident(&variant.name.to_lower_camel_case());
        out.push_str(&format!("        case \"{}\":\n", variant_tag));

        if variant.fields.is_empty() {
            out.push_str(&format!("            self = .{}\n", case_name));
        } else {
            out.push_str(&format!("            self = .{}(", case_name));
            for (i, field) in variant.fields.iter().enumerate() {
                let label = swift_associated_label(&field.name, i);
                let already_optional = matches!(&field.ty, TypeRef::Optional(_));
                let is_optional = field.optional || already_optional;
                let ty = mapper.map_type(&field.ty);

                if is_optional {
                    // For Optional types, use decodeIfPresent with the type as-is
                    out.push_str(&format!(
                        "{}: try container.decodeIfPresent({}.self, forKey: .{})",
                        label, ty, label
                    ));
                } else {
                    out.push_str(&format!(
                        "{}: try container.decode({}.self, forKey: .{})",
                        label, ty, label
                    ));
                }
                if i < variant.fields.len() - 1 {
                    out.push_str(", ");
                }
            }
            out.push_str(")\n");
        }
    }

    out.push_str("        default:\n");
    out.push_str("            throw DecodingError.dataCorruptedError(\n");
    out.push_str(&format!("                forKey: .{},\n", tag_key));
    out.push_str("                in: container,\n");
    out.push_str(&format!(
        "                debugDescription: \"Unknown {} type: \\(type)\"\n",
        en.name
    ));
    out.push_str("            )\n");
    out.push_str("        }\n");
    out.push_str("    }\n\n");

    // Emit encode(to:)
    out.push_str("    public func encode(to encoder: Encoder) throws {\n");
    out.push_str("        var container = encoder.container(keyedBy: CodingKeys.self)\n");
    out.push_str("        switch self {\n");

    for variant in &en.variants {
        // Compute the variant tag value
        let variant_tag = if let Some(rename) = &variant.serde_rename {
            rename.clone()
        } else {
            match en.serde_rename_all.as_deref() {
                Some("camelCase") => variant.name.to_lower_camel_case(),
                Some("snake_case") => variant.name.to_snake_case(),
                Some("SCREAMING_SNAKE_CASE") => variant.name.to_snake_case().to_uppercase(),
                Some("PascalCase") => variant.name.to_upper_camel_case(),
                Some("kebab-case") => variant.name.to_kebab_case(),
                Some("lowercase") => variant.name.to_lowercase(),
                Some("UPPERCASE") => variant.name.to_uppercase(),
                _ => variant.name.clone(),
            }
        };

        let case_name = swift_case_ident(&variant.name.to_lower_camel_case());

        if variant.fields.is_empty() {
            out.push_str(&format!("        case .{}:\n", case_name));
            out.push_str(&format!(
                "            try container.encode(\"{}\", forKey: .{})\n",
                variant_tag, tag_key
            ));
        } else {
            let mut bindings = Vec::new();
            for (i, field) in variant.fields.iter().enumerate() {
                let label = swift_associated_label(&field.name, i);
                bindings.push(format!("let {}", label));
            }
            out.push_str(&format!("        case .{}({}):\n", case_name, bindings.join(", ")));
            out.push_str(&format!(
                "            try container.encode(\"{}\", forKey: .{})\n",
                variant_tag, tag_key
            ));

            for (i, field) in variant.fields.iter().enumerate() {
                let label = swift_associated_label(&field.name, i);
                let already_optional = matches!(&field.ty, TypeRef::Optional(_));
                let is_optional = field.optional || already_optional;

                if is_optional {
                    out.push_str(&format!(
                        "            try container.encodeIfPresent({}, forKey: .{})\n",
                        label, label
                    ));
                } else {
                    out.push_str(&format!(
                        "            try container.encode({}, forKey: .{})\n",
                        label, label
                    ));
                }
            }
        }
    }

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

/// Emits a custom Codable conformance for a `#[serde(untagged)]` enum so
/// Swift round-trips the same wire shape as serde — i.e. a bare value
/// (`"foo"`, `[1,2,3]`, `{...}`) rather than the externally-tagged
/// `{"variant": payload}` shape that `Codable`'s auto-synthesised
/// implementation produces for enum data variants.
///
/// Each variant must have exactly one positional payload (the `field0`
/// label emitted by `swift_associated_label`). The init tries each variant
/// in declaration order via `singleValueContainer().decode(T.self)` until
/// one succeeds, mirroring serde's untagged deserialiser; if none match,
/// throws `DecodingError.dataCorruptedError`. Encoder writes the payload
/// value directly into a single-value container.
fn emit_serde_untagged_codable(en: &EnumDef, out: &mut String, mapper: &SwiftMapper) {
    // init(from:)
    out.push_str("    public init(from decoder: Decoder) throws {\n");
    out.push_str("        let container = try decoder.singleValueContainer()\n");
    for variant in &en.variants {
        if variant.fields.len() != 1 {
            continue;
        }
        let case_name = swift_case_ident(&variant.name.to_lower_camel_case());
        let payload_ty = mapper.map_type(&variant.fields[0].ty);
        let label = swift_associated_label(&variant.fields[0].name, 0);
        out.push_str(&format!(
            "        if let value = try? container.decode({payload_ty}.self) {{\n            self = .{case_name}({label}: value)\n            return\n        }}\n"
        ));
    }
    out.push_str(
        "        throw DecodingError.dataCorruptedError(in: container, debugDescription: \"no matching untagged variant\")\n",
    );
    out.push_str("    }\n\n");

    // encode(to:)
    out.push_str("    public func encode(to encoder: Encoder) throws {\n");
    out.push_str("        var container = encoder.singleValueContainer()\n");
    out.push_str("        switch self {\n");
    for variant in &en.variants {
        if variant.fields.len() != 1 {
            continue;
        }
        let case_name = swift_case_ident(&variant.name.to_lower_camel_case());
        let label = swift_associated_label(&variant.fields[0].name, 0);
        out.push_str(&format!(
            "        case .{case_name}(let {label}):\n            try container.encode({label})\n"
        ));
    }
    out.push_str("        }\n");
    out.push_str("    }\n");
}

/// Emits a Swift enum or typealias for the given `EnumDef`.
/// Non-Codable enums (`has_serde: false`) become typealiases to RustBridge.X.
/// Codable enums (`has_serde: true`) are emitted as native Swift enums.
///
/// When `needs_codable` is `true` (the enum is a field type of a streaming item
/// Codable struct), all-unit enums receive `: String, Codable` conformance so
/// `JSONDecoder` can decode them. The raw value for each case is the `serde`
/// serialized form derived from `serde_rename_all` (or the camelCase variant
/// name when no rename strategy is set).
fn emit_enum(
    en: &EnumDef,
    out: &mut String,
    mapper: &SwiftMapper,
    known_dto_names: &std::collections::HashSet<String>,
) {
    emit_doc_comment(&en.doc, "", out);

    if !en.has_serde {
        out.push_str(&crate::backends::swift::template_env::render(
            "typealias.jinja",
            minijinja::context! {
                name => &en.name,
            },
        ));
        return;
    }

    let all_unit = en.variants.iter().all(|v| v.fields.is_empty());

    if all_unit {
        // All-unit serde enums receive `: String, Codable, Sendable, Hashable` conformance
        // so that structs with enum-typed fields can derive `Codable` automatically.
        // Raw values are the serde-serialized variant names (respecting serde_rename_all
        // and per-variant serde_rename overrides) so JSONDecoder/JSONEncoder round-trip
        // correctly against the Rust wire format.
        let _ = mapper; // mapper unused for case bodies — suppress unused warning
        out.push_str(&format!(
            "public enum {}: String, Codable, Sendable, Hashable {{\n",
            en.name
        ));
        for variant in &en.variants {
            emit_doc_comment(&variant.doc, "    ", out);
            let case_name = swift_case_ident(&variant.name.to_lower_camel_case());
            let raw_value = unit_enum_raw_value(variant, en.serde_rename_all.as_deref());
            if raw_value == case_name.trim_matches('`') {
                // Raw value matches the Swift case name — no explicit annotation needed.
                out.push_str(&crate::backends::swift::template_env::render(
                    "enum_case_unit.jinja",
                    minijinja::context! {
                        case_name => &case_name,
                    },
                ));
            } else {
                // Explicit raw-value annotation required (e.g. `case toolCalls = "tool_calls"`).
                out.push_str(&format!("    case {case_name} = \"{raw_value}\"\n"));
            }
        }
        out.push_str("}\n");
        emit_enum_into_rust_extension(&en.name, out);
        return;
    }
    // Data-variant enum (has_serde + not all_unit): emit as native Swift enum with associated values.
    //
    // If the enum has serde_tag (internally-tagged), emit custom Codable conformance
    // to handle the {"type": "variant", ...} wire format instead of Swift's default
    // {"variant": {...}} externally-tagged form.
    //
    // Otherwise, rely on Swift's auto-synthesized Codable when all associated value
    // types are Codable-safe (primitives, String, or known first-class structs).
    // When any associated value is not Codable-safe, fall back to a typealias to
    // RustBridge.X — users lose Swift-side pattern matching but everything else works.
    if !all_variants_codable_safe(en, known_dto_names) {
        out.push_str(&crate::backends::swift::template_env::render(
            "typealias.jinja",
            minijinja::context! {
                name => &en.name,
            },
        ));
        return;
    }

    // Check if this enum uses serde tagging (internally-tagged) or untagged.
    let has_serde_tag = en.serde_tag.is_some() && !en.serde_untagged;
    let is_serde_untagged = en.serde_untagged
        && en.variants.iter().any(|v| !v.fields.is_empty())
        && en
            .variants
            .iter()
            .filter(|v| !v.fields.is_empty())
            .all(|v| v.fields.len() == 1);

    if has_serde_tag {
        // Emit as enum with custom Codable for internally-tagged format
        out.push_str(&format!("public enum {}: Codable, Sendable, Hashable {{\n", en.name));
        for variant in &en.variants {
            emit_variant_with_data(variant, out, mapper);
        }
        out.push('\n');
        emit_serde_tagged_codable(en, out, mapper);
        out.push_str("}\n");
    } else if is_serde_untagged {
        // Emit as enum with custom Codable that matches serde's untagged
        // wire format (bare value, no `{"variant": ...}` wrapper).
        out.push_str(&format!("public enum {}: Codable, Sendable, Hashable {{\n", en.name));
        for variant in &en.variants {
            emit_variant_with_data(variant, out, mapper);
        }
        out.push('\n');
        emit_serde_untagged_codable(en, out, mapper);
        out.push_str("}\n");
    } else {
        // Emit as enum with auto-synthesized Codable (externally-tagged)
        out.push_str(&crate::backends::swift::template_env::render(
            "swift_enum_header.jinja",
            minijinja::context! {
                name => &en.name,
            },
        ));
        for variant in &en.variants {
            emit_variant_with_data(variant, out, mapper);
        }
        out.push_str("}\n");
    }

    emit_enum_into_rust_extension(&en.name, out);
}

/// Returns `true` when every associated value type across `en.variants` is safe to
/// reference inside a Codable+Sendable+Hashable Swift enum — i.e. the type is a
/// primitive, String/Char/Path/Json/Bytes/Duration, or a Named type already known
/// to be a first-class Codable struct or unit serde enum.
fn all_variants_codable_safe(en: &EnumDef, known_dto_names: &std::collections::HashSet<String>) -> bool {
    use crate::core::ir::TypeRef;
    fn supported(ty: &TypeRef, known: &std::collections::HashSet<String>) -> bool {
        match ty {
            TypeRef::Primitive(_)
            | TypeRef::String
            | TypeRef::Char
            | TypeRef::Path
            | TypeRef::Json
            | TypeRef::Unit
            | TypeRef::Bytes
            | TypeRef::Duration => true,
            TypeRef::Named(n) => known.contains(n),
            TypeRef::Optional(inner) | TypeRef::Vec(inner) => supported(inner, known),
            TypeRef::Map(k, v) => supported(k, known) && supported(v, known),
        }
    }
    en.variants
        .iter()
        .flat_map(|v| v.fields.iter())
        .all(|f| supported(&f.ty, known_dto_names))
}

/// Emits a Swift enum for a trait-bridge result type without an intoRust() extension.
/// Result-type enums are first-class enums that JSON-decode locally in Swift and do NOT
/// call a Rust-side from_json function, so they don't need the FFI round-trip mechanism.
fn emit_enum_without_into_rust(
    en: &EnumDef,
    out: &mut String,
    mapper: &SwiftMapper,
    known_dto_names: &std::collections::HashSet<String>,
) {
    emit_doc_comment(&en.doc, "", out);

    if !en.has_serde {
        out.push_str(&crate::backends::swift::template_env::render(
            "typealias.jinja",
            minijinja::context! {
                name => &en.name,
            },
        ));
        return;
    }

    let all_unit = en.variants.iter().all(|v| v.fields.is_empty());

    if all_unit {
        // Emit all-unit enum without intoRust() extension (unlike emit_enum).
        let _ = mapper;
        out.push_str(&format!(
            "public enum {}: String, Codable, Sendable, Hashable {{\n",
            en.name
        ));
        for variant in &en.variants {
            emit_doc_comment(&variant.doc, "    ", out);
            let case_name = swift_case_ident(&variant.name.to_lower_camel_case());
            let raw_value = unit_enum_raw_value(variant, en.serde_rename_all.as_deref());
            if raw_value == case_name.trim_matches('`') {
                out.push_str(&crate::backends::swift::template_env::render(
                    "enum_case_unit.jinja",
                    minijinja::context! {
                        case_name => &case_name,
                    },
                ));
            } else {
                out.push_str(&format!("    case {case_name} = \"{raw_value}\"\n"));
            }
        }
        out.push_str("}\n");
        // Do NOT call emit_enum_into_rust_extension — that's the key difference.
        return;
    }

    // For data-variant enums, check if all variants are Codable-safe.
    if all_variants_codable_safe(en, known_dto_names) {
        // Emit as a native Swift enum with associated values, without intoRust().
        out.push_str(&format!("public enum {}: Codable, Sendable, Hashable {{\n", en.name));
        for variant in &en.variants {
            emit_variant_with_data(variant, out, mapper);
        }
        out.push_str("}\n");
        // Do NOT call emit_enum_into_rust_extension — that's the key difference.
    } else {
        // Fall back to typealias (same as emit_enum) if not all variants are Codable-safe.
        out.push_str(&crate::backends::swift::template_env::render(
            "typealias.jinja",
            minijinja::context! {
                name => &en.name,
            },
        ));
    }
}

/// Emits an `extension {EnumName} { func intoRust() throws -> RustBridge.{EnumName} { ... } }`
/// block that converts a public Swift enum back to its opaque FFI-bridge counterpart.
///
/// The bridge enum (a swift-bridge `extern "Rust" { type {Name}; }`) is exposed as an
/// opaque Swift class with no public initializer — there is no way to construct one from
/// individual cases. To round-trip across the FFI boundary, the extension JSON-encodes
/// `self` and delegates to the `{enum_snake}_from_json` swift-bridge shim emitted by the
/// Rust bridge crate. This mirrors the JSON-fallback path used in struct `intoRust()`
/// emission (see `emit_first_class_struct`) and gives every enum the same shape of
/// reverse-conversion that first-class structs already have.
///
/// `throws` because `JSONEncoder.encode` and the Rust shim both fail on malformed
/// input. The extension is internal-visibility (no `public` keyword): callers are other
/// generated bindings inside the same module, never end-users.
fn emit_enum_into_rust_extension(name: &str, out: &mut String) {
    let from_json_fn = format!("{}_from_json", AsSnakeCase(name)).to_lower_camel_case();
    out.push_str(&format!("extension {name} {{\n"));
    out.push_str(&format!("    func intoRust() throws -> RustBridge.{name} {{\n"));
    out.push_str("        let data = try JSONEncoder().encode(self)\n");
    out.push_str("        let json = String(data: data, encoding: .utf8) ?? \"null\"\n");
    out.push_str(&format!("        return try RustBridge.{from_json_fn}(json)\n"));
    out.push_str("    }\n");
    out.push_str("}\n");
}

/// Returns the serde wire value for a unit enum variant.
///
/// Priority:
/// 1. Per-variant `serde_rename` override (verbatim).
/// 2. Enum-level `serde_rename_all` strategy applied to the Rust PascalCase variant name.
/// 3. The Rust PascalCase variant name unchanged (serde default).
///
/// Supported `serde_rename_all` values: `"snake_case"`, `"camelCase"`, `"SCREAMING_SNAKE_CASE"`,
/// `"kebab-case"`. Unknown strategies fall back to the PascalCase variant name.
fn unit_enum_raw_value(variant: &crate::core::ir::EnumVariant, rename_all: Option<&str>) -> String {
    use heck::{ToKebabCase, ToLowerCamelCase, ToSnakeCase, ToUpperCamelCase};
    if let Some(rename) = &variant.serde_rename {
        return rename.clone();
    }
    match rename_all {
        Some("snake_case") => variant.name.to_snake_case(),
        Some("camelCase") => variant.name.to_lower_camel_case(),
        Some("SCREAMING_SNAKE_CASE") | Some("SCREAMING-KEBAB-CASE") => variant.name.to_snake_case().to_uppercase(),
        Some("PascalCase") => variant.name.to_upper_camel_case(),
        Some("kebab-case") | Some("train-case") => variant.name.to_kebab_case(),
        Some("lowercase") => variant.name.to_lowercase(),
        Some("UPPERCASE") => variant.name.to_uppercase(),
        _ => variant.name.clone(),
    }
}

/// Emits a single enum case, with or without associated values.
fn emit_variant_with_data(variant: &EnumVariant, out: &mut String, mapper: &SwiftMapper) {
    emit_doc_comment(&variant.doc, "    ", out);
    let case_name = swift_case_ident(&variant.name.to_lower_camel_case());
    if variant.fields.is_empty() {
        out.push_str(&crate::backends::swift::template_env::render(
            "enum_case_unit.jinja",
            minijinja::context! {
                case_name => &case_name,
            },
        ));
    } else {
        let assoc: Vec<String> = variant
            .fields
            .iter()
            .enumerate()
            .map(|(idx, f)| {
                // Honor field.optional (extractor-unwrapped form) in addition to
                // TypeRef::Optional(inner) — both encode "nullable" in the IR.
                let already_optional = matches!(&f.ty, TypeRef::Optional(_));
                let ty_str = mapper.map_type(&f.ty);
                let ty_with_opt = if f.optional && !already_optional {
                    format!("{ty_str}?")
                } else {
                    ty_str
                };
                let label = swift_associated_label(&f.name, idx);
                format!("{label}: {ty_with_opt}")
            })
            .collect();
        out.push_str(&crate::backends::swift::template_env::render(
            "enum_case_with_data.jinja",
            minijinja::context! {
                case_name => &case_name,
                associated_values => assoc.join(", "),
            },
        ));
    }
}

/// Resolves a Swift associated-value label for an enum case field.
///
/// - Empty, all-digit, or `_<digits>` names (positional tuple variants) become
///   `field0`, `field1`, …
/// - Otherwise lowerCamelCase + Swift-idiomatic backtick keyword escaping
///   (associated-value labels appear in emitted Swift source).
fn swift_associated_label(name: &str, idx: usize) -> String {
    let stripped = name.trim_start_matches('_');
    if stripped.is_empty() || stripped.chars().all(|c| c.is_ascii_digit()) {
        return format!("field{idx}");
    }
    swift_case_ident(&name.to_lower_camel_case())
}

/// Emits a Swift `Swift.Error`-conforming `public enum` for the given `ErrorDef`.
///
/// When the Rust error type is named `Error`, Swift would parse `public enum Error: Error`
/// as a circular raw-type binding rather than protocol conformance. In that case the enum
/// is renamed to `{module_name}Error` (e.g. `TreeSitterLanguagePackError`) to avoid the
/// clash. The protocol reference is always qualified as `Swift.Error` for clarity.
fn emit_error(error: &ErrorDef, module_name: &str, out: &mut String, mapper: &SwiftMapper) {
    // Rename bare `Error` to `{ModuleName}Error` to avoid the Swift parser ambiguity
    // where `public enum Error: Error` is interpreted as a circular raw-type binding
    // instead of protocol conformance.
    let name = if error.name == "Error" {
        format!("{module_name}Error")
    } else {
        error.name.clone()
    };
    emit_doc_comment(&error.doc, "", out);
    out.push_str(&crate::backends::swift::template_env::render(
        "error_enum_header.jinja",
        minijinja::context! {
            name => &name,
        },
    ));
    for variant in &error.variants {
        emit_doc_comment(&variant.doc, "    ", out);
        let case_name = swift_case_ident(&variant.name.to_lower_camel_case());
        if variant.is_unit || variant.fields.is_empty() {
            out.push_str(&crate::backends::swift::template_env::render(
                "error_case.jinja",
                minijinja::context! {
                    case_name => &case_name,
                },
            ));
        } else {
            let mut assoc: Vec<String> = Vec::with_capacity(variant.fields.len() + 1);
            let mut seen_message = false;
            let mut labels: BTreeSet<String> = BTreeSet::new();
            for (idx, f) in variant.fields.iter().enumerate() {
                // Honor field.optional (extractor-unwrapped form) in addition to
                // TypeRef::Optional(inner) — both encode "nullable" in the IR.
                let already_optional = matches!(&f.ty, TypeRef::Optional(_));
                let ty_str = mapper.map_type(&f.ty);
                let ty_with_opt = if f.optional && !already_optional {
                    format!("{ty_str}?")
                } else {
                    ty_str
                };
                let mut label = swift_associated_label(&f.name, idx);
                // Disambiguate duplicate labels by suffixing the index.
                while labels.contains(&label) {
                    label = format!("{label}{idx}");
                }
                labels.insert(label.clone());
                if label == "message" {
                    seen_message = true;
                }
                assoc.push(format!("{label}: {ty_with_opt}"));
            }
            if !seen_message {
                assoc.insert(0, "message: String".to_string());
            }
            out.push_str(&crate::backends::swift::template_env::render(
                "error_case_with_data.jinja",
                minijinja::context! {
                    case_name => &case_name,
                    associated_values => assoc.join(", "),
                },
            ));
        }
    }
    out.push_str("}\n");
    // Emit a public extension with computed properties for each whitelisted
    // introspection method (e.g. `status_code`, `is_transient`, `error_type`).
    // Each property switches over `self` and delegates to the per-variant
    // associated values or returns a sensible default when the variant carries
    // no such field.  Backends that wire a swift-bridge free function can
    // replace these stubs in a subsequent code-generation pass.
    if !error.methods.is_empty() {
        out.push('\n');
        out.push_str(&format!("extension {name} {{\n"));
        for method in &error.methods {
            let prop_name = swift_case_ident(&method.name.to_lower_camel_case());
            let return_ty = swift_type_name(&method.return_type);
            let default_val = swift_default_for_type(&method.return_type);
            out.push_str(&format!("    public var {prop_name}: {return_ty} {{\n"));
            out.push_str("        switch self {\n");
            for variant in &error.variants {
                let case_name = swift_case_ident(&variant.name.to_lower_camel_case());
                // Check whether this variant carries an associated value whose
                // name matches the method (e.g. `status_code` ↔ `status`).
                let field_match = variant.fields.iter().find(|f| {
                    let camel = f.name.to_lower_camel_case();
                    let prop_snake = method.name.as_str();
                    // Exact match or common abbreviation (status_code → status).
                    camel == prop_name
                        || f.name == prop_snake
                        || (prop_snake == "status_code" && (f.name == "status" || camel == "status"))
                });
                let wildcard = if variant.is_unit || variant.fields.is_empty() {
                    String::new()
                } else {
                    let mut args: Vec<String> = variant
                        .fields
                        .iter()
                        .enumerate()
                        .map(|(i, f)| {
                            let label = swift_associated_label(&f.name, i);
                            if let Some(fm) = &field_match {
                                if fm.name == f.name {
                                    return format!("{label}: let matched");
                                }
                            }
                            format!("{label}: _")
                        })
                        .collect();
                    // The case declaration above synthesizes a leading
                    // `message: String` parameter when none of the original
                    // fields is named `message`.  The switch pattern must
                    // include the same synthetic label or the tuple lengths
                    // will mismatch.
                    let has_message_field = variant.fields.iter().any(|f| f.name == "message");
                    if !has_message_field {
                        args.insert(0, "message: _".to_string());
                    }
                    format!("({})", args.join(", "))
                };
                let ret_expr = if field_match.is_some() && !variant.is_unit && !variant.fields.is_empty() {
                    "matched".to_string()
                } else {
                    default_val.clone()
                };
                out.push_str(&format!("        case .{case_name}{wildcard}: return {ret_expr}\n"));
            }
            out.push_str("        }\n");
            out.push_str("    }\n");
        }
        out.push_str("}\n");
    }
}

/// Returns the Swift zero/default literal for a given `TypeRef`.
fn swift_default_for_type(ty: &TypeRef) -> String {
    match ty {
        TypeRef::Primitive(p) => {
            use crate::core::ir::PrimitiveType;
            match p {
                PrimitiveType::Bool => "false".to_string(),
                _ => "0".to_string(),
            }
        }
        TypeRef::String => "\"\"".to_string(),
        TypeRef::Optional(_) => "nil".to_string(),
        _ => "nil".to_string(),
    }
}

/// Emits a Swift `public final class` wrapper for an opaque Rust type that exposes methods.
///
/// The class holds an inner `RustBridge.<TypeName>` instance and re-exposes each method
/// via a public Swift API that delegates to a free function generated by the bridge crate.
/// The constructor reads `api_key` and optional `base_url` from Swift, matching the
/// `createDefaultClient`/`defaultClientChat` pattern produced in the Rust bridge crate.
///
/// Naming conventions (all snake-case in Rust, all camelCase in Swift):
/// - Constructor bridge fn: `create_<snake_type_name>` → camelCase `create<PascalTypeName>`
/// - Method bridge fn: `<snake_type_name>_<method_name>` → camelCase `<camelTypeName><PascalMethod>`
fn emit_client_class(
    type_name: &str,
    methods: &[MethodDef],
    mapper: &impl TypeMapper,
    config: &ResolvedCrateConfig,
    first_class_types: &std::collections::HashSet<String>,
    out: &mut String,
) {
    use heck::ToSnakeCase;

    // Swift-bridge free functions bridging this class.
    let snake_name = type_name.to_snake_case();
    let constructor_fn = swift_ident(&format!("create_{snake_name}").to_lower_camel_case());

    out.push_str(&format!("public final class {type_name} {{\n"));
    out.push_str(&format!("    private let inner: RustBridge.{type_name}\n"));
    out.push_str("    public init(apiKey: String, baseUrl: String? = nil) throws {\n");
    // swift-bridge generates the constructor as a free function with positional
    // parameters (no argument labels). Calling it with `apiKey:baseUrl:` labels
    // would not match the generated signature.
    out.push_str(&format!(
        "        self.inner = try RustBridge.{constructor_fn}(apiKey, baseUrl)\n"
    ));
    out.push_str("    }\n");
    // Internal init from the raw bridge handle so free-function forwarders
    // that construct the client through alternate Rust APIs (e.g.
    // `create_client(api_key, base_url, timeout_secs, ...)` or
    // `create_client_from_json(json)`) can wrap their bridge return into
    // the public Swift class without going through the limited
    // `init(apiKey:baseUrl:)` constructor.
    out.push_str(&format!("    internal init(_ inner: RustBridge.{type_name}) {{\n"));
    out.push_str("        self.inner = inner\n");
    out.push_str("    }\n");

    for method in methods {
        // Skip async-less, sanitized, or static methods.
        if method.sanitized {
            continue;
        }
        let method_snake = method.name.to_snake_case();
        let method_camel = swift_ident(&method_snake.to_lower_camel_case());
        let bridge_fn_snake = format!("{snake_name}_{method_snake}");
        let bridge_fn_camel = swift_ident(&bridge_fn_snake.to_lower_camel_case());

        // Build parameter list for the Swift public method (excluding self/receiver).
        let params: Vec<String> = method
            .params
            .iter()
            .map(|p| {
                let swift_name = swift_ident(&p.name.to_lower_camel_case());
                let ty_str = if p.optional {
                    format!("{}?", mapper.map_type(&p.ty))
                } else {
                    mapper.map_type(&p.ty)
                };
                format!("_ {swift_name}: {ty_str}")
            })
            .collect();
        let params_str = params.join(", ");

        // Build argument list for forwarding to the bridge function.
        // When a parameter type is a first-class Swift DTO (i.e. has `intoRust()`
        // emitted), the bridge function expects the raw `RustBridge.T` type, not
        // the Swift wrapper `T`.  Apply `.intoRust()` at the call site.
        // `intoRust()` is `throws`, so track whether any conversion is needed so we
        // can add `throws` to the method signature even when `error_type` is `None`.
        let has_dto_param = method
            .params
            .iter()
            .any(|p| matches!(&p.ty, TypeRef::Named(n) if first_class_types.contains(n)));
        let args: Vec<String> = method
            .params
            .iter()
            .map(|p| {
                let swift_name = swift_ident(&p.name.to_lower_camel_case());
                match &p.ty {
                    TypeRef::Named(n) if first_class_types.contains(n) => {
                        // Optional first-class DTO: `query?.intoRust()` returns
                        // `RustBridge.T?` which matches the swift-bridge-emitted Optional<T>
                        // signature.
                        if p.optional {
                            format!("try {swift_name}?.intoRust()")
                        } else {
                            format!("try {swift_name}.intoRust()")
                        }
                    }
                    _ => swift_name,
                }
            })
            .collect();
        let args_str = if args.is_empty() {
            String::new()
        } else {
            format!(", {}", args.join(", "))
        };

        let return_ty = mapper.map_type(&method.return_type);
        // First-class DTO return type: the bridge call returns the opaque `RustBridge.T`,
        // but the Swift method signature exposes the first-class `T` struct. Wrap the call
        // in `try T(bridgeReturn)` using the `init(_ rb: RustBridge.T) throws` extension
        // emitted by `emit_first_class_struct`.
        let needs_return_init = matches!(
            &method.return_type,
            TypeRef::Named(n) if first_class_types.contains(n)
        );
        let needs_throws = method.error_type.is_some() || has_dto_param || needs_return_init;
        let throws_clause = if needs_throws { " throws" } else { "" };
        let async_clause = if method.is_async { " async" } else { "" };
        let return_clause = if matches!(method.return_type, TypeRef::Unit) {
            String::new()
        } else {
            format!(" -> {return_ty}")
        };

        // Emit `/// doc` when available — routed through the shared template helper so
        // method documentation matches the formatting used for types, enums, and errors.
        emit_doc_comment(&method.doc, "    ", out);
        out.push_str(&format!(
            "    public func {method_camel}({params_str}){async_clause}{throws_clause}{return_clause} {{\n"
        ));
        if matches!(method.return_type, TypeRef::Unit) {
            out.push_str(&format!(
                "        {throws_kw}RustBridge.{bridge_fn_camel}(self.inner{args_str})\n",
                throws_kw = if needs_throws { "try " } else { "" }
            ));
        } else {
            let await_kw = if method.is_async { "await " } else { "" };
            let try_kw = if needs_throws { "try " } else { "" };
            // swift-bridge bridges `Vec<u8>` as `RustVec<UInt8>` on the Swift side.
            // The host wrapper exposes `Data` (per `SwiftMapper::bytes()`) — convert
            // by iterating the RustVec into a Swift array, then wrapping in `Data`.
            let bytes_suffix = if matches!(method.return_type, TypeRef::Bytes) {
                ".map { Data($0.map { $0 }) }"
            } else {
                ""
            };
            if bytes_suffix.is_empty() {
                if needs_return_init {
                    out.push_str(&format!(
                        "        return try {return_ty}({try_kw}{await_kw}RustBridge.{bridge_fn_camel}(self.inner{args_str}))\n"
                    ));
                } else {
                    out.push_str(&format!(
                        "        return {try_kw}{await_kw}RustBridge.{bridge_fn_camel}(self.inner{args_str})\n"
                    ));
                }
            } else {
                // For Bytes returns we can't chain `.map` on a throwing call directly,
                // so capture the RustVec into a local and convert.
                out.push_str(&format!(
                    "        let _bytes = {try_kw}{await_kw}RustBridge.{bridge_fn_camel}(self.inner{args_str})\n"
                ));
                out.push_str("        return Data(_bytes.map { $0 })\n");
            }
        }
        out.push_str("    }\n");
    }

    // Emit wrapper methods for streaming adapters owned by this client type.
    // These adapters (pattern = `streaming`) are not part of the IR `methods` list —
    // they are defined in `config.adapters` and generate a dedicated bridge function
    // in the Rust crate.  Emit a public `async throws` wrapper that calls the
    // corresponding RustBridge free function so Swift callers can initiate a stream
    // (errors like HTTP 401 propagate before any chunks arrive, making the function throw).
    for adapter in config
        .adapters
        .iter()
        .filter(|a| matches!(a.pattern, AdapterPattern::Streaming))
        .filter(|a| a.owner_type.as_deref() == Some(type_name))
    {
        emit_streaming_client_method(adapter, &snake_name, first_class_types, out);
    }

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

/// Emit an `AsyncThrowingStream<Item, Error>` wrapper for a streaming adapter
/// method on a Swift client class.
///
/// Streaming adapters (e.g. `chatStream`) are bridged via three pieces in the
/// generated swift-bridge Rust crate (see
/// `gen_rust_crate::wrappers::emit_streaming_adapter_shims`):
///
/// 1. An opaque `{Owner}{Adapter}StreamHandle` Rust struct owning a tokio runtime +
///    boxed stream. swift-bridge generates the matching Swift class with `deinit`.
/// 2. A `{owner_snake}_{adapter}_start(client, params...) -> Result<Handle, String>`
///    free function. HTTP-level errors surface here before any chunks arrive.
/// 3. A `next(&mut self) -> Result<String, String>` method on the handle —
///    returns the JSON-encoded chunk, `""` on clean end-of-stream, or `Err` on a
///    stream-level error.
///
/// The Swift wrapper:
///
/// - Returns `AsyncThrowingStream<ItemType, Error>` for `for try await chunk in stream` use.
/// - Runs `_start` on a detached high-priority Task to keep the calling executor unblocked.
/// - Drives `handle.next()` from a second detached Task, calling
///   `RustBridge.{itemType}FromJson(json)` to deserialise each chunk into the opaque type
///   so callers can use the full swift-bridge method API on the yielded values.
/// - Empty-string response from `next()` finishes the stream cleanly.
/// - `continuation.onTermination` cancels the drain task so the handle's `deinit`
///   runs promptly when a Swift consumer early-breaks out of the for-await loop.
fn emit_streaming_client_method(
    adapter: &AdapterConfig,
    owner_snake: &str,
    first_class_types: &std::collections::HashSet<String>,
    out: &mut String,
) {
    use heck::{AsSnakeCase, ToLowerCamelCase};
    let method_camel = swift_ident(&adapter.name.to_lower_camel_case());
    let start_fn_snake = format!("{owner_snake}_{}_start", adapter.name);
    let start_fn_camel = swift_ident(&start_fn_snake.to_lower_camel_case());

    let item_type = adapter.item_type.as_deref().unwrap_or("String");

    // Compute the `from_json` Swift bridge function name for this item type.
    // e.g. `ChatCompletionChunk` → `chatCompletionChunkFromJson`
    let item_type_from_json = swift_ident(&format!("{}_from_json", AsSnakeCase(item_type)).to_lower_camel_case());

    // Build the Swift parameter list from the adapter params.
    let params: Vec<String> = adapter
        .params
        .iter()
        .map(|p| {
            let swift_name = swift_ident(&p.name.to_lower_camel_case());
            let simple_ty = p.ty.rsplit("::").next().unwrap_or(&p.ty);
            format!("_ {swift_name}: {simple_ty}")
        })
        .collect();
    let params_str = params.join(", ");

    // Build argument list for forwarding to the bridge function.
    // For first-class struct params (Codable structs with intoRust()) we must
    // convert to the swift-bridge opaque type before passing across the FFI
    // boundary, because the bridge function signature expects `{Type}Ref` (a
    // class), not the first-class `struct {Type}`.
    let call_args: Vec<String> = adapter
        .params
        .iter()
        .map(|p| {
            let swift_name = swift_ident(&p.name.to_lower_camel_case());
            let simple_ty = p.ty.rsplit("::").next().unwrap_or(&p.ty);
            if first_class_types.contains(simple_ty) {
                // intoRust() can throw; the Task.detached closure propagates throws.
                format!("(try {swift_name}.intoRust())")
            } else {
                swift_name
            }
        })
        .collect();
    let call_args_str = if call_args.is_empty() {
        String::new()
    } else {
        format!(", {}", call_args.join(", "))
    };

    // Open the streaming method.
    out.push_str(&format!(
        "    public func {method_camel}({params_str}) async throws -> AsyncThrowingStream<{item_type}, Error> {{\n"
    ));
    // Kick the stream on a detached Task so the calling executor stays free
    // while the request opens (which itself blocks on a tokio runtime inside Rust).
    out.push_str("        let inner = self.inner\n");
    out.push_str("        let handle = try await Task.detached(priority: .userInitiated) {\n");
    out.push_str(&format!(
        "            try RustBridge.{start_fn_camel}(inner{call_args_str})\n"
    ));
    out.push_str("        }.value\n\n");

    // Build the AsyncThrowingStream. The drain Task owns the handle and is
    // cancelled by `onTermination` so the handle's deinit fires promptly.
    out.push_str(&format!(
        "        return AsyncThrowingStream<{item_type}, Error> {{ continuation in\n"
    ));
    out.push_str("            let task = Task.detached(priority: .userInitiated) {\n");
    out.push_str("                do {\n");
    out.push_str("                    while !Task.isCancelled {\n");
    out.push_str("                        let json = try handle.next().toString()\n");
    out.push_str("                        if json.isEmpty { break }\n");
    if first_class_types.contains(item_type) {
        // First-class struct: decode with JSONDecoder directly — no RustBridge shim.
        out.push_str("                        let chunkData = json.data(using: .utf8) ?? Data()\n");
        out.push_str(&format!(
            "                        let chunk = try JSONDecoder().decode({item_type}.self, from: chunkData)\n"
        ));
    } else {
        out.push_str(&format!(
            "                        let chunk = try RustBridge.{item_type_from_json}(json)\n"
        ));
    }
    out.push_str("                        continuation.yield(chunk)\n");
    out.push_str("                    }\n");
    out.push_str("                    continuation.finish()\n");
    out.push_str("                } catch {\n");
    out.push_str("                    continuation.finish(throwing: error)\n");
    out.push_str("                }\n");
    out.push_str("            }\n");
    out.push_str("            continuation.onTermination = { _ in task.cancel() }\n");
    out.push_str("        }\n");
    out.push_str("    }\n");
}

/// Emit an `extension RustBridge.{Owner}{Adapter}StreamHandle: @unchecked Sendable {}`
/// declaration for the given streaming adapter.
///
/// swift-bridge generates opaque Swift classes without `Sendable` conformance.
/// The generated streaming wrapper passes the handle across `Task.detached` boundaries,
/// which Swift 6 strict-concurrency rejects unless the type is `Sendable`.  The Rust
/// implementation wraps a `Mutex<stream>` and a `tokio::runtime::Runtime` — both
/// thread-safe — so `@unchecked Sendable` is the correct annotation: Rust enforces the
/// invariant, Swift just cannot see it.
///
/// The extension must reference the `RustBridge.` prefix because the handle type lives
/// in the swift-bridge–generated `RustBridge` module, not in this module.
fn emit_stream_handle_sendable(adapter: &AdapterConfig, owner_type: &str, out: &mut String) {
    use heck::ToPascalCase;
    let owner_pascal = owner_type.to_pascal_case();
    let adapter_pascal = adapter.name.to_pascal_case();
    let handle_name = format!("{owner_pascal}{adapter_pascal}StreamHandle");
    out.push_str(&format!("// MARK: - Sendable conformance for {handle_name}\n"));
    out.push_str("// swift-bridge opaque types are not automatically Sendable.  The Rust\n");
    out.push_str("// side uses Mutex<stream> + tokio Runtime — both Send + Sync — so\n");
    out.push_str("// @unchecked is correct: thread-safety is enforced by Rust.\n");
    out.push_str(&format!(
        "extension RustBridge.{handle_name}: @unchecked Sendable {{}}\n"
    ));
}

/// Emit top-level `public func` streaming wrappers for adapters whose owner type
/// is **not** wrapped in a Swift client class (i.e. no `client_constructor_body` in
/// alef.toml).  For those types the `emit_streaming_client_method` path is never
/// reached because `emit_client_class` is skipped.
///
/// Instead we emit a free function that takes the owner handle as its first
/// parameter, followed by the adapter params, and returns the same
/// `AsyncThrowingStream<Item, Error>` that the class-method variant would return.
///
/// The function also emits `@unchecked Sendable` extensions for the owner type and
/// the stream-handle type so Swift 6 strict-concurrency accepts capturing them in
/// `Task.detached` closures (both are backed by Rust `Send + Sync` types).
fn emit_streaming_free_functions(
    config: &ResolvedCrateConfig,
    first_class_types: &std::collections::HashSet<String>,
    out: &mut String,
) {
    use heck::{AsSnakeCase, ToLowerCamelCase, ToSnakeCase};

    let client_constructor_types: std::collections::HashSet<&str> = config
        .swift
        .as_ref()
        .map(|c| c.client_constructor_body.keys().map(String::as_str).collect())
        .unwrap_or_default();

    let orphan_adapters: Vec<&AdapterConfig> = config
        .adapters
        .iter()
        .filter(|a| matches!(a.pattern, AdapterPattern::Streaming))
        .filter(|a| !a.skip_languages.iter().any(|l| l == "swift"))
        .filter(|a| {
            a.owner_type
                .as_deref()
                .map(|t| !client_constructor_types.contains(t))
                .unwrap_or(false)
        })
        .collect();

    if orphan_adapters.is_empty() {
        return;
    }

    out.push_str("// MARK: - Streaming free functions\n");
    out.push_str(
        "// These adapters are owned by opaque handle types that do not have a\n\
         // Swift class wrapper (no client_constructor_body in alef.toml).  The\n\
         // streaming methods are therefore exposed as module-level free functions\n\
         // that accept the owner handle as their first parameter.\n\n",
    );

    let mut sendable_emitted: std::collections::HashSet<String> = std::collections::HashSet::new();
    for adapter in &orphan_adapters {
        let owner_type = adapter.owner_type.as_deref().unwrap_or("");
        if sendable_emitted.insert(owner_type.to_string()) {
            out.push_str(&format!(
                "// MARK: - Sendable conformance for {owner_type} (streaming owner)\n"
            ));
            out.push_str("// swift-bridge opaque types are not automatically Sendable.\n");
            out.push_str("// Captured by Task.detached in streaming free functions — Rust type is Send + Sync.\n");
            out.push_str(&format!(
                "extension RustBridge.{owner_type}: @unchecked Sendable {{}}\n\n"
            ));
        }
        emit_stream_handle_sendable(adapter, owner_type, out);
        out.push('\n');
        for param in &adapter.params {
            let simple_ty = param.ty.rsplit("::").next().unwrap_or(&param.ty);
            if !first_class_types.contains(simple_ty) {
                let key = format!("param:{simple_ty}");
                if sendable_emitted.insert(key) {
                    out.push_str(&format!(
                        "// MARK: - Sendable conformance for {simple_ty} (streaming request param)\n"
                    ));
                    out.push_str("// swift-bridge opaque types are not automatically Sendable.\n");
                    out.push_str("// Passed into Task.detached for streaming — Rust type is Send + Sync.\n");
                    out.push_str(&format!(
                        "extension RustBridge.{simple_ty}: @unchecked Sendable {{}}\n\n"
                    ));
                }
            }
        }
    }

    for adapter in &orphan_adapters {
        let owner_type = adapter.owner_type.as_deref().unwrap_or("");
        let owner_snake = owner_type.to_snake_case();
        let method_camel = swift_ident(&adapter.name.to_lower_camel_case());
        let start_fn_snake = format!("{owner_snake}_{}_start", adapter.name);
        let start_fn_camel = swift_ident(&start_fn_snake.to_lower_camel_case());

        let item_type = adapter.item_type.as_deref().unwrap_or("String");
        let item_type_from_json = swift_ident(&format!("{}_from_json", AsSnakeCase(item_type)).to_lower_camel_case());

        let owner_camel = swift_ident(&owner_type.to_lower_camel_case());
        let mut sig_params: Vec<String> = vec![format!("_ {owner_camel}: {owner_type}")];
        let mut call_args: Vec<String> = vec![];

        for param in &adapter.params {
            let swift_name = swift_ident(&param.name.to_lower_camel_case());
            let simple_ty = param.ty.rsplit("::").next().unwrap_or(&param.ty);
            sig_params.push(format!("_ {swift_name}: {simple_ty}"));
            if first_class_types.contains(simple_ty) {
                call_args.push(format!("(try {swift_name}.intoRust())"));
            } else {
                call_args.push(swift_name);
            }
        }

        let params_str = sig_params.join(", ");
        let call_args_str = if call_args.is_empty() {
            String::new()
        } else {
            format!(", {}", call_args.join(", "))
        };

        out.push_str(&format!(
            "public func {method_camel}({params_str}) async throws -> AsyncThrowingStream<{item_type}, Error> {{\n"
        ));
        out.push_str("    let handle = try await Task.detached(priority: .userInitiated) {\n");
        out.push_str(&format!(
            "        try RustBridge.{start_fn_camel}({owner_camel}{call_args_str})\n"
        ));
        out.push_str("    }.value\n\n");
        out.push_str(&format!(
            "    return AsyncThrowingStream<{item_type}, Error> {{ continuation in\n"
        ));
        out.push_str("        let task = Task.detached(priority: .userInitiated) {\n");
        out.push_str("            do {\n");
        out.push_str("                while !Task.isCancelled {\n");
        out.push_str("                    let json = try handle.next().toString()\n");
        out.push_str("                    if json.isEmpty { break }\n");
        if first_class_types.contains(item_type) {
            out.push_str("                    let chunkData = json.data(using: .utf8) ?? Data()\n");
            out.push_str(&format!(
                "                    let chunk = try JSONDecoder().decode({item_type}.self, from: chunkData)\n"
            ));
        } else {
            out.push_str(&format!(
                "                    let chunk = try RustBridge.{item_type_from_json}(json)\n"
            ));
        }
        out.push_str("                    continuation.yield(chunk)\n");
        out.push_str("                }\n");
        out.push_str("                continuation.finish()\n");
        out.push_str("            } catch {\n");
        out.push_str("                continuation.finish(throwing: error)\n");
        out.push_str("            }\n");
        out.push_str("        }\n");
        out.push_str("        continuation.onTermination = { _ in task.cancel() }\n");
        out.push_str("    }\n");
        out.push_str("}\n\n");
    }
}

/// Emits `/// <line>` doc-comment lines with the given indent prefix.
fn emit_doc_comment(doc: &str, indent: &str, out: &mut String) {
    if doc.is_empty() {
        return;
    }
    out.push_str(&crate::backends::swift::template_env::render(
        "doc_comment.jinja",
        minijinja::context! {
            indent => indent,
            lines => doc.lines().collect::<Vec<_>>(),
        },
    ));
}

/// Returns `true` if the first parameter of `func_def` has the given `TypeRef` shape.
fn first_param_is(func_def: &FunctionDef, ty: &TypeRef) -> bool {
    func_def.params.first().map(|p| &p.ty == ty).unwrap_or(false)
}

/// Emits Swift convenience overloads for functions whose first parameter is
/// `TypeRef::Bytes` or `TypeRef::Path`.
///
/// Detection is entirely IR-driven — no function names are hardcoded. The emitter
/// scans `api.functions` for non-async functions that:
///
/// - Have `TypeRef::Bytes` as their first parameter → emit a `String` overload
///   (UTF-8 encoded) and a `[UInt8]` overload, both delegating to the sync bridge
///   function via `makeByteVec`.
/// - Have `TypeRef::Path` as their first parameter → emit a `String` path overload
///   with the remaining parameters forwarded unchanged.
///
/// The `makeByteVec` helper converts a Swift `[UInt8]` to `RustVec<UInt8>` using
/// the push-based API that swift-bridge exposes at runtime.
///
/// ## Skipped emissions
///
/// A convenience overload is skipped when the public wrapper name would collide
/// with the bridge function name (`wrapper_name == swift_inner`). Swift overload
/// resolution prefers the labeled overload from the *current* file when looking
/// up the inner positional call, so the wrapper would recursively resolve to
/// itself instead of the unlabeled bridge function. There is no clean way to
/// disambiguate (module-prefix qualification of free functions is rejected by
/// the Swift compiler). Users needing a `String`/`[UInt8]` ergonomic flavor for
/// these functions can call the bridge function directly with `makeByteVec`.
fn emit_convenience_wrappers(api: &ApiSurface, out: &mut String) {
    // Collect the names of all functions in the surface for O(1) lookup.
    let all_names: std::collections::HashSet<&str> = api.functions.iter().map(|f| f.name.as_str()).collect();

    let bytes_candidates: Vec<&FunctionDef> = api
        .functions
        .iter()
        .filter(|f| first_param_is(f, &TypeRef::Bytes) && !f.is_async && !convenience_name_shadows_bridge(f))
        .collect();

    let path_candidates: Vec<&FunctionDef> = api
        .functions
        .iter()
        .filter(|f| first_param_is(f, &TypeRef::Path) && !f.is_async && !convenience_name_shadows_bridge(f))
        .collect();

    if bytes_candidates.is_empty() && path_candidates.is_empty() {
        return;
    }

    out.push_str("// MARK: - Convenience Wrapper Functions\n");
    out.push_str("// These wrappers bridge String / [UInt8] inputs to RustBridge's\n");
    out.push_str("// RustVec<UInt8> requirement. The config parameter must be a fully\n");
    out.push_str("// constructed opaque type (built via the generated initializer);\n");
    out.push_str("// JSON-config decoding is not available because swift-bridge opaque\n");
    out.push_str("// proxy classes are not Codable Swift structs.\n\n");

    // Only emit makeByteVec when there is at least one bytes candidate.
    if !bytes_candidates.is_empty() {
        out.push_str("/// Converts a Swift `[UInt8]` array to a `RustVec<UInt8>` by pushing each byte.\n");
        out.push_str("/// swift-bridge's `RustVec<T>` runtime only exposes `init()` and `push(value:)`;\n");
        out.push_str("/// no array-initializer shorthand exists.\n");
        out.push_str("private func makeByteVec(_ bytes: [UInt8]) -> RustVec<UInt8> {\n");
        out.push_str("    let vec = RustVec<UInt8>()\n");
        out.push_str("    for b in bytes { vec.push(value: b) }\n");
        out.push_str("    return vec\n");
        out.push_str("}\n\n");
    }

    for func in &bytes_candidates {
        emit_bytes_overloads(func, &all_names, out);
    }

    for func in &path_candidates {
        emit_path_overload(func, &all_names, out);
    }

    let _ = api;
}

/// Emit public Swift forwarding functions for every serde-enabled, non-opaque type
/// that appears as a method/function parameter and therefore has a
/// `#[swift_bridge(swift_name = "{TypeName}FromJson")]` shim in the Rust bridge crate.
///
/// The Rust bridge crate exposes these shims in the `RustBridge` module. Without a
/// forwarding wrapper here, callers would need to write `RustBridge.chatCompletionRequestFromJson(json)`.
/// These forwarders re-export them as `public func chatCompletionRequestFromJson(_ json: String)
/// throws -> TypeName` in the main `LiterLlm` module so generated e2e tests work without
/// the `RustBridge.` prefix.
fn emit_from_json_forwarders(
    api: &ApiSurface,
    exclude_types: &std::collections::HashSet<String>,
    mapper: &SwiftMapper,
    exclude_fields: &std::collections::HashSet<String>,
    known_dto_names: &std::collections::HashSet<String>,
    out: &mut String,
) {
    use heck::AsSnakeCase;

    // Emit `*FromJson` forwarders for every from_json-eligible type — i.e. every
    // serde-enabled, non-opaque, non-trait type in the api surface.  This must
    // stay a strict superset of the union of types for which the Rust crate side
    // (`gen_rust_crate::emit_lib_rs`) emits `{type_snake}_from_json` swift-bridge
    // shims, which is itself the union of three sets:
    //   1. `collect_serde_param_types` (types used as method/free-fn params)
    //   2. streaming item types
    //   3. `json_fallback_types` (types Swift's `intoRust()` JSON-encodes)
    // The previous filter mirrored only (2), so types that participate purely
    // as nested fields / return types (LayoutDetectionConfig, PdfConfig,
    // PostProcessorConfig, etc.) — but are still first-class Codable structs on
    // the Swift side — lacked a top-level `*FromJson` forwarder. Broaden to the
    // shared structural predicate (`!is_trait && !is_opaque && has_serde`) so
    // every Swift e2e fixture that names a config type can decode JSON without
    // routing through `RustBridge.`.
    let struct_candidates: Vec<&str> = api
        .types
        .iter()
        .filter(|t| !t.is_trait && !t.is_opaque && t.has_serde)
        .filter(|t| !exclude_types.contains(&t.name))
        .map(|t| t.name.as_str())
        .collect();

    // Enums with serde derives are emitted as Swift `Codable` enums by `emit_enum`
    // (either `enum X: String, Codable` for all-unit or `enum X: Codable` with
    // associated values for data variants). They also need a top-level
    // `{enumSnake}FromJson` forwarder so e2e fixtures can decode enum JSON, and
    // because subagent 1's enum `intoRust()` extension calls
    // `RustBridge.{enumSnake}FromJson(json)` after JSON-encoding `self`.
    let enum_candidates: Vec<&str> = api
        .enums
        .iter()
        .filter(|e| e.has_serde && !exclude_types.contains(&e.name))
        .map(|e| e.name.as_str())
        .collect();

    if struct_candidates.is_empty() && enum_candidates.is_empty() {
        return;
    }

    out.push_str("// MARK: - From-JSON Helpers\n");
    out.push_str("// Public helpers that decode JSON into first-class Swift types.\n");
    out.push_str("// First-class struct types (Codable) use JSONDecoder directly.\n");
    out.push_str("// Opaque RustBridge types forward to RustBridge.\n\n");

    // First-class structs have serde + non-empty fields — decode with JSONDecoder.
    let first_class_set: std::collections::HashSet<&str> = api
        .types
        .iter()
        .filter(|t| !t.is_trait && can_emit_first_class_struct(t, mapper, exclude_fields, known_dto_names))
        .map(|t| t.name.as_str())
        .collect();

    for type_name in struct_candidates {
        let type_snake = AsSnakeCase(type_name).to_string();
        let swift_name = format!("{type_snake}_from_json").to_lower_camel_case();
        if first_class_set.contains(type_name) {
            out.push_str(&format!(
                "public func {swift_name}(_ json: String) throws -> {type_name} {{\n    let data = json.data(using: .utf8) ?? Data()\n    return try JSONDecoder().decode({type_name}.self, from: data)\n}}\n\n"
            ));
        } else {
            out.push_str(&format!(
                "public func {swift_name}(_ json: String) throws -> {type_name} {{\n    return try RustBridge.{swift_name}(json)\n}}\n\n"
            ));
        }
    }

    // Serde-enabled enums are emitted either as native Swift `Codable` enums
    // (unit-only or data-variant with bridge-safe associated values) or as
    // typealiases to the opaque `RustBridge.{Enum}` wrapper when the data
    // variants reference non-Codable-synthesisable types. The `*FromJson`
    // forwarder routes accordingly: Codable enums use `JSONDecoder`; typealias
    // enums forward to the Rust-side `RustBridge.{enum}FromJson` shim because
    // `JSONDecoder().decode(RustBridge.{Enum}.self, ...)` would fail (opaque
    // bridge classes are not Codable). The Rust-side shim is unconditionally
    // emitted for every serde-enabled enum by `gen_rust_crate::emit_lib_rs`.
    let codable_enum_set: std::collections::HashSet<&str> = api
        .enums
        .iter()
        .filter(|e| e.has_serde && enum_emits_codable(e, known_dto_names))
        .map(|e| e.name.as_str())
        .collect();
    for enum_name in enum_candidates {
        let enum_snake = AsSnakeCase(enum_name).to_string();
        let swift_name = format!("{enum_snake}_from_json").to_lower_camel_case();
        if codable_enum_set.contains(enum_name) {
            out.push_str(&format!(
                "public func {swift_name}(_ json: String) throws -> {enum_name} {{\n    let data = json.data(using: .utf8) ?? Data()\n    return try JSONDecoder().decode({enum_name}.self, from: data)\n}}\n\n"
            ));
        } else {
            out.push_str(&format!(
                "public func {swift_name}(_ json: String) throws -> {enum_name} {{\n    return try RustBridge.{swift_name}(json)\n}}\n\n"
            ));
        }
    }
}

/// Returns `true` when `emit_enum` would emit `en` as a native Codable Swift
/// enum (either `enum X: String, Codable` for all-unit serde enums or
/// `enum X: Codable` with associated values for bridge-safe data variants),
/// rather than falling back to the `typealias X = RustBridge.X` opaque path.
///
/// This must mirror the conditional logic in [`emit_enum`] exactly so that the
/// `*FromJson` forwarder's JSONDecoder-vs-RustBridge routing decision lines up
/// with the actual Swift declaration of the enum.
fn enum_emits_codable(en: &crate::core::ir::EnumDef, known_dto_names: &std::collections::HashSet<String>) -> bool {
    if !en.has_serde {
        return false;
    }
    let all_unit = en.variants.iter().all(|v| v.fields.is_empty());
    if all_unit {
        return true;
    }
    all_variants_codable_safe(en, known_dto_names)
}

/// Emits String and [UInt8] convenience overloads for a function whose first param
/// is `TypeRef::Bytes`. The overloads delegate to the function itself via `makeByteVec`.
///
/// The wrapper function name is derived by stripping the `_sync` suffix from the
/// function name (if present) and converting to camelCase. The inner call uses
/// the camelCased name of the function directly (which swift-bridge exposes).
///
/// The convenience overload always uses a labeled first parameter (`content:`),
/// while the underlying bridge function exposes an unlabeled first parameter
/// (`_ content: RustVec<UInt8>`). Swift overload resolution distinguishes these
/// by argument label inclusion, so a positional inner call `swift_inner(makeByteVec(...))`
/// resolves unambiguously to the bridge function even when `wrapper_name == swift_inner`.
///
/// We previously qualified the inner call with `RustBridge.` to "bypass shadowing",
/// but Swift does not allow `ModuleName.functionName` qualification of free functions
/// imported from another module — the call `RustBridge.detectMimeTypeFromBytes(...)`
/// fails with "module 'RustBridge' has no member named 'detectMimeTypeFromBytes'".
fn emit_bytes_overloads(func: &FunctionDef, _all_names: &std::collections::HashSet<&str>, out: &mut String) {
    let swift_inner = swift_ident(&func.name.to_lower_camel_case());
    // Wrapper name: strip trailing "Sync" for the public-facing overload name.
    let wrapper_name = if swift_inner.ends_with("Sync") {
        swift_inner[..swift_inner.len() - 4].to_string()
    } else {
        swift_inner.clone()
    };
    // Inner call is always the unqualified bridge function name. Argument-label
    // disambiguation handles the case where wrapper_name == swift_inner.
    let inner_call = swift_inner.clone();

    // Collect remaining params (everything after the first Bytes param).
    let trailing_params: Vec<&crate::core::ir::ParamDef> = func.params.iter().skip(1).collect();

    // Return type name (Named types are used as Swift type names directly).
    let return_ty = swift_return_type(&func.return_type);
    let throws_clause = if func.error_type.is_some() { " throws" } else { "" };
    let return_suffix = swift_return_conversion_suffix(&func.return_type);

    let trailing_param_text = render_trailing_params(trailing_params.iter().copied());
    let trailing_args = render_trailing_args(trailing_params.iter().copied());

    out.push_str(&crate::backends::swift::template_env::render(
        "swift_bytes_string_overload.jinja",
        minijinja::context! {
            wrapper_name => &wrapper_name,
            trailing_params => &trailing_param_text,
            throws_clause => throws_clause,
            return_ty => &return_ty,
            inner_call => &inner_call,
            trailing_args => &trailing_args,
            return_suffix => &return_suffix,
        },
    ));

    out.push_str(&crate::backends::swift::template_env::render(
        "swift_bytes_array_overload.jinja",
        minijinja::context! {
            wrapper_name => &wrapper_name,
            trailing_params => &trailing_param_text,
            throws_clause => throws_clause,
            return_ty => &return_ty,
            inner_call => &inner_call,
            trailing_args => &trailing_args,
            return_suffix => &return_suffix,
        },
    ));
}

/// Emits a String path convenience overload for a function whose first param is
/// `TypeRef::Path`. The wrapper accepts `path: String` instead of a `URL` or `Path`
/// and delegates to the camelCased bridge function directly.
///
/// The convenience overload uses the labeled `path:` first parameter; the bridge
/// function uses an unlabeled first parameter, so positional inner calls resolve
/// unambiguously without `RustBridge.` qualification (which Swift rejects for
/// free functions imported from another module).
fn emit_path_overload(func: &FunctionDef, _all_names: &std::collections::HashSet<&str>, out: &mut String) {
    let swift_inner = swift_ident(&func.name.to_lower_camel_case());
    let wrapper_name = if swift_inner.ends_with("Sync") {
        swift_inner[..swift_inner.len() - 4].to_string()
    } else {
        swift_inner.clone()
    };
    let inner_call = swift_inner.clone();

    let trailing_params: Vec<&crate::core::ir::ParamDef> = func.params.iter().skip(1).collect();
    let return_ty = swift_return_type(&func.return_type);
    let throws_clause = if func.error_type.is_some() { " throws" } else { "" };
    let return_suffix = swift_return_conversion_suffix(&func.return_type);

    let trailing_param_text = render_trailing_params_with_defaults(trailing_params.iter().copied());
    let trailing_args = render_trailing_args(trailing_params.iter().copied());

    out.push_str(&crate::backends::swift::template_env::render(
        "swift_path_overload.jinja",
        minijinja::context! {
            wrapper_name => &wrapper_name,
            trailing_params => &trailing_param_text,
            throws_clause => throws_clause,
            return_ty => &return_ty,
            inner_call => &inner_call,
            trailing_args => &trailing_args,
            return_suffix => &return_suffix,
        },
    ));
}

/// Emits trailing parameters (after the first) as `, paramName: Type` additions
/// to the current function signature line, without default values.
fn render_trailing_params<'a>(params: impl Iterator<Item = &'a crate::core::ir::ParamDef>) -> String {
    let mut out = String::new();
    for p in params {
        let swift_name = p.name.to_lower_camel_case();
        let ty_str = if p.optional {
            format!("{}?", swift_type_name(&p.ty))
        } else {
            swift_type_name(&p.ty)
        };
        out.push_str(&crate::backends::swift::template_env::render(
            "swift_trailing_param.jinja",
            minijinja::context! {
                swift_name => &swift_name,
                ty_str => &ty_str,
            },
        ));
    }
    out
}

/// Like `emit_trailing_params` but emits ` = nil` for optional parameters,
/// producing Swift-idiomatic default arguments.
fn render_trailing_params_with_defaults<'a>(params: impl Iterator<Item = &'a crate::core::ir::ParamDef>) -> String {
    let mut out = String::new();
    for p in params {
        let swift_name = p.name.to_lower_camel_case();
        if p.optional {
            let ty_str = swift_type_name(&p.ty);
            out.push_str(&crate::backends::swift::template_env::render(
                "swift_trailing_param_optional_default.jinja",
                minijinja::context! {
                    swift_name => &swift_name,
                    ty_str => &ty_str,
                },
            ));
        } else {
            let ty_str = swift_type_name(&p.ty);
            out.push_str(&crate::backends::swift::template_env::render(
                "swift_trailing_param.jinja",
                minijinja::context! {
                    swift_name => &swift_name,
                    ty_str => &ty_str,
                },
            ));
        }
    }
    out
}

fn render_trailing_args<'a>(params: impl Iterator<Item = &'a crate::core::ir::ParamDef>) -> String {
    let mut out = String::new();
    for p in params {
        let swift_name = p.name.to_lower_camel_case();
        out.push_str(&crate::backends::swift::template_env::render(
            "swift_trailing_arg.jinja",
            minijinja::context! {
                swift_name => &swift_name,
            },
        ));
    }
    out
}

/// Maps a `TypeRef` to its Swift type name string for use in function signatures.
/// Only covers the subset of types that appear in convenience-overload signatures.
fn swift_type_name(ty: &TypeRef) -> String {
    match ty {
        TypeRef::String => "String".to_string(),
        TypeRef::Bytes => "[UInt8]".to_string(),
        TypeRef::Path => "String".to_string(),
        TypeRef::Named(name) => name.clone(),
        TypeRef::Optional(inner) => format!("{}?", swift_type_name(inner)),
        TypeRef::Vec(inner) => format!("[{}]", swift_type_name(inner)),
        TypeRef::Map(k, v) => format!("[{}: {}]", swift_type_name(k), swift_type_name(v)),
        TypeRef::Primitive(p) => {
            use crate::core::ir::PrimitiveType;
            match p {
                PrimitiveType::Bool => "Bool",
                PrimitiveType::U8 => "UInt8",
                PrimitiveType::U16 => "UInt16",
                PrimitiveType::U32 => "UInt32",
                PrimitiveType::U64 => "UInt64",
                PrimitiveType::I8 => "Int8",
                PrimitiveType::I16 => "Int16",
                PrimitiveType::I32 => "Int32",
                PrimitiveType::I64 => "Int64",
                PrimitiveType::Usize => "UInt",
                PrimitiveType::Isize => "Int",
                PrimitiveType::F32 => "Float",
                PrimitiveType::F64 => "Double",
            }
            .to_string()
        }
        TypeRef::Unit => "Void".to_string(),
        TypeRef::Json => "String".to_string(),
        TypeRef::Duration => "Duration".to_string(),
        TypeRef::Char => "Character".to_string(),
    }
}

/// Maps a function's return `TypeRef` to the Swift return type string.
fn swift_return_type(ty: &TypeRef) -> String {
    swift_type_name(ty)
}

/// Returns the conversion suffix needed to bridge swift-bridge's runtime return
/// types to the Swift-native types declared in the convenience-wrapper
/// signatures.
///
/// swift-bridge maps a Rust `String` return (bare or inside `Result<String, _>`)
/// directly to a Swift-native `String` — no `RustString` wrapping. Likewise, a
/// `Result<T, _>` return is unwrapped through `try`, leaving the bare Swift
/// type. The only conversions we need to apply are for `RustVec<T>` returns
/// (Bytes, `Vec<primitive>`), which are exposed as a `Sequence` of native
/// elements but not as a Swift `Array` — we use `.map { $0 }` to materialise.
///
/// Returns the empty string for return types that need no conversion (Swift
/// `String`, opaque `Named` types, primitives, `Void`).
fn swift_return_conversion_suffix(ty: &TypeRef) -> String {
    match ty {
        TypeRef::Bytes => ".map { $0 }".to_string(),
        TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Primitive(_)) => ".map { $0 }".to_string(),
        _ => String::new(),
    }
}

/// Returns true when emitting a convenience overload for `func` would produce a
/// public wrapper whose name collides with the bridge function name (i.e. the
/// camelCased function name without a `_sync` suffix to strip). When that
/// happens, the unlabeled positional inner call inside the wrapper resolves to
/// the wrapper itself (Swift's overload resolution prefers in-file declarations
/// over imports, and the convenience overload's labels do not disambiguate
/// against unlabeled candidates because Swift treats labeled-vs-unlabeled as
/// the same overload key when no exact label match exists). The result is a
/// "no exact matches in call to global function" compile error.
///
/// Module-prefix qualification (`RustBridge.fn(...)`) is rejected by the Swift
/// compiler for free functions imported from another module. We therefore skip
/// emitting the convenience overload entirely; the bridge function is still
/// callable directly with `makeByteVec(...)`.
fn convenience_name_shadows_bridge(func: &FunctionDef) -> bool {
    let swift_inner = swift_ident(&func.name.to_lower_camel_case());
    let wrapper_name = if swift_inner.ends_with("Sync") {
        swift_inner[..swift_inner.len() - 4].to_string()
    } else {
        swift_inner.clone()
    };
    wrapper_name == swift_inner
}

/// Locate the swift-bridge build output directory under `target/`.
///
/// swift-bridge-build writes its output during `cargo build -p {binding_crate}` to
/// `target/{profile}/build/{binding_crate}-{hash}/out/`.  We walk both `release` and
/// `debug` profiles and return the most-recently-modified `out/` directory that
/// contains `SwiftBridgeCore.swift`.
///
/// Returns `None` when no build output is found (i.e. `cargo build` has not been run
/// yet), allowing the caller to skip the copy step without failing.
fn find_swift_bridge_out_dir(binding_crate_name: &str) -> Option<PathBuf> {
    let cwd = std::env::current_dir().ok()?;
    // Search workspace root: try cwd, then parent dirs, until we find a Cargo.lock.
    let workspace_root = std::iter::once(cwd.clone())
        .chain(cwd.ancestors().skip(1).map(|p| p.to_path_buf()))
        .take(8)
        .find(|p| p.join("Cargo.lock").exists())?;
    let target = workspace_root.join("target");

    let crate_prefix = format!("{binding_crate_name}-");

    let mut best: Option<(std::time::SystemTime, PathBuf)> = None;
    // Prefer `release` over `debug` by iterating release first: when mtimes are
    // equal (e.g. on a fresh checkout where both profiles were built together),
    // the release build wins and we avoid accidentally picking a debug build that
    // may have been built with a smaller feature set.
    for profile in ["release", "debug"] {
        let build_dir = target.join(profile).join("build");
        let entries = match std::fs::read_dir(&build_dir) {
            Ok(e) => e,
            Err(_) => continue,
        };
        for entry in entries.flatten() {
            let name = entry.file_name();
            let name_str = name.to_string_lossy();
            if !name_str.starts_with(&crate_prefix) {
                continue;
            }
            let out = entry.path().join("out");
            // Use the mtime of the marker *file* (written by swift_bridge_build)
            // rather than the directory mtime.  Directory mtimes are updated by
            // filesystem reads on some macOS configurations and can therefore be
            // newer than the actual build without reflecting fresh content.  File
            // mtime is set precisely when cargo writes the build output.
            let marker = out.join("SwiftBridgeCore.swift");
            if !marker.exists() {
                continue;
            }
            let mtime = std::fs::metadata(&marker)
                .and_then(|m| m.modified())
                .unwrap_or(std::time::UNIX_EPOCH);
            if best.as_ref().map(|(t, _)| mtime > *t).unwrap_or(true) {
                best = Some((mtime, out));
            }
        }
    }
    best.map(|(_, p)| p)
}

/// Emit the swift-bridge generated files (`SwiftBridgeCore.swift`,
/// `{crate}-swift.swift`, and `RustBridgeC.h`) into the SwiftPM package layout.
///
/// This is called from `generate_bindings` after the IR-based bindings and Rust crate
/// have been emitted. When the cargo build output is not yet present (first run before
/// `cargo build -p {crate}-swift`), the function returns `None` and the scaffold
/// placeholders remain on disk untouched.
///
/// The two Swift files receive `import RustBridgeC\n` prepended so the SwiftPM
/// `RustBridge` target can resolve the C types declared in the `RustBridgeC` target.
/// `RustBridgeC.h` is emitted as a thin umbrella `#include` header that pulls in both
/// `SwiftBridgeCore.h` and `{binding_crate}.h` from the same directory.
fn emit_swift_bridge_files(
    crate_name: &str,
    binding_crate_name: &str,
    package_root: &std::path::Path,
) -> anyhow::Result<Option<Vec<GeneratedFile>>> {
    let out_dir = match find_swift_bridge_out_dir(binding_crate_name) {
        Some(d) => d,
        None => {
            // No build output yet.  Upgrade the committed placeholder `RustBridgeC.h` to
            // include C structs used by swift-bridge's generated `SwiftBridgeCore.swift`.
            // Without these typedefs the Swift compiler reports "cannot find type" errors
            // for RustStr, __private__FfiSlice, and __private__OptionXX extensions
            // in the committed `SwiftBridgeCore.swift`.
            let sources_rust_bridge_c = package_root.join("Sources").join("RustBridgeC");
            let header_path = sources_rust_bridge_c.join("RustBridgeC.h");
            // Only emit the minimal placeholder when the existing header still matches the
            // old form (no __private__FfiSlice typedef).  If the full header has already been
            // written by a prior generate run with build output, leave it alone.
            let needs_upgrade = header_path
                .exists()
                .then(|| std::fs::read_to_string(&header_path).ok())
                .flatten()
                .map(|content| !content.contains("__private__FfiSlice"))
                .unwrap_or(false);
            if needs_upgrade {
                let minimal_header = format!(
                    "#ifndef RUST_BRIDGE_C_H\n\
                     #define RUST_BRIDGE_C_H\n\
                     \n\
                     // Placeholder header for the RustBridgeC SwiftPM target.\n\
                     // Run `cargo build -p {binding_crate_name}` and re-run `alef generate` to populate.\n\
                     // The typedefs below are the minimum required for SwiftBridgeCore.swift\n\
                     // to compile before the full cargo build has been run.\n\
                     \n\
                     #include <stdint.h>\n\
                     #include <stdbool.h>\n\
                     \n\
                     typedef struct RustStr {{ uint8_t* const start; uintptr_t len; }} RustStr;\n\
                     typedef struct __private__FfiSlice {{ void* const start; uintptr_t len; }} __private__FfiSlice;\n\
                     typedef struct __private__OptionU8 {{ uint8_t val; bool is_some; }} __private__OptionU8;\n\
                     typedef struct __private__OptionI8 {{ int8_t val; bool is_some; }} __private__OptionI8;\n\
                     typedef struct __private__OptionU16 {{ uint16_t val; bool is_some; }} __private__OptionU16;\n\
                     typedef struct __private__OptionI16 {{ int16_t val; bool is_some; }} __private__OptionI16;\n\
                     typedef struct __private__OptionU32 {{ uint32_t val; bool is_some; }} __private__OptionU32;\n\
                     typedef struct __private__OptionI32 {{ int32_t val; bool is_some; }} __private__OptionI32;\n\
                     typedef struct __private__OptionU64 {{ uint64_t val; bool is_some; }} __private__OptionU64;\n\
                     typedef struct __private__OptionI64 {{ int64_t val; bool is_some; }} __private__OptionI64;\n\
                     typedef struct __private__OptionUsize {{ uintptr_t val; bool is_some; }} __private__OptionUsize;\n\
                     typedef struct __private__OptionIsize {{ intptr_t val; bool is_some; }} __private__OptionIsize;\n\
                     typedef struct __private__OptionF32 {{ float val; bool is_some; }} __private__OptionF32;\n\
                     typedef struct __private__OptionF64 {{ double val; bool is_some; }} __private__OptionF64;\n\
                     typedef struct __private__OptionBool {{ bool val; bool is_some; }} __private__OptionBool;\n\
                     \n\
                     #endif /* RUST_BRIDGE_C_H */\n"
                );
                return Ok(Some(vec![GeneratedFile {
                    path: header_path,
                    content: minimal_header,
                    generated_header: false,
                }]));
            }
            return Ok(None);
        }
    };

    // SwiftBridgeCore.swift: top-level in out/
    let core_swift_src = out_dir.join("SwiftBridgeCore.swift");
    // {crate}-swift.swift: inside out/{binding_crate}/
    let crate_swift_src = out_dir
        .join(binding_crate_name)
        .join(format!("{binding_crate_name}.swift"));
    // SwiftBridgeCore.h + {crate}-swift.h for the umbrella header
    let core_h_src = out_dir.join("SwiftBridgeCore.h");
    let crate_h_src = out_dir.join(binding_crate_name).join(format!("{binding_crate_name}.h"));

    // If any required file is missing, skip silently — the build may be stale.
    for p in [&core_swift_src, &crate_swift_src, &core_h_src, &crate_h_src] {
        if !p.exists() {
            return Ok(None);
        }
    }

    let core_swift = std::fs::read_to_string(&core_swift_src)
        .map_err(|e| anyhow::anyhow!("failed to read {}: {e}", core_swift_src.display()))?;
    let crate_swift = std::fs::read_to_string(&crate_swift_src)
        .map_err(|e| anyhow::anyhow!("failed to read {}: {e}", crate_swift_src.display()))?;
    let core_h = std::fs::read_to_string(&core_h_src)
        .map_err(|e| anyhow::anyhow!("failed to read {}: {e}", core_h_src.display()))?;
    let crate_h = std::fs::read_to_string(&crate_h_src)
        .map_err(|e| anyhow::anyhow!("failed to read {}: {e}", crate_h_src.display()))?;

    // Prepend `import RustBridgeC` to each Swift file.
    // swift-bridge's generated SwiftBridgeCore.swift starts with `import Foundation`; the
    // crate-specific swift file has no imports at all.  Both reference C types (RustStr,
    // __private__Option*, __swift_bridge__$Vec_*) that live in the RustBridgeC SwiftPM target.
    let core_swift_content = append_rust_string_ref_to_string_extension(&prepend_rust_bridge_c_import(&core_swift));
    let crate_swift_content = prepend_rust_bridge_c_import(&crate_swift);

    // RustBridgeC.h: umbrella header concatenating both generated C headers.
    // SwiftPM exposes the directory via `publicHeadersPath: "."`, so every `.h` file
    // in Sources/RustBridgeC/ is automatically visible to dependent targets.
    // Using a single concatenated umbrella keeps the directory layout simple.
    let rust_bridge_c_h = format!(
        "#ifndef RUST_BRIDGE_C_H\n\
         #define RUST_BRIDGE_C_H\n\
         \n\
         // Auto-generated by alef — do not edit by hand.\n\
         // Concatenates SwiftBridgeCore.h and {binding_crate_name}.h produced by\n\
         // `cargo build -p {binding_crate_name}` via swift_bridge_build.\n\
         \n\
         {core_h}\n\
         {crate_h}\n\
         #endif /* RUST_BRIDGE_C_H */\n"
    );

    let sources_rust_bridge = package_root.join("Sources").join("RustBridge");
    let sources_rust_bridge_c = package_root.join("Sources").join("RustBridgeC");
    // Use crate_name (e.g. "sample-llm") for the filename, not binding_crate_name.
    // swift-bridge names the sub-directory after the binding crate, but the output
    // Swift file is conventionally named after the crate being bridged.
    let _ = crate_name; // used via binding_crate_name for path, crate_name kept for clarity
    let files = vec![
        GeneratedFile {
            path: sources_rust_bridge.join("SwiftBridgeCore.swift"),
            content: core_swift_content,
            generated_header: false,
        },
        GeneratedFile {
            path: sources_rust_bridge.join(format!("{binding_crate_name}.swift")),
            content: crate_swift_content,
            generated_header: false,
        },
        GeneratedFile {
            path: sources_rust_bridge_c.join("RustBridgeC.h"),
            content: rust_bridge_c_h,
            generated_header: false,
        },
    ];
    Ok(Some(files))
}

// ---------------------------------------------------------------------------
// Inbound protocol emission (OptionsField trait bridges)
// ---------------------------------------------------------------------------

/// Emit Swift protocol + adapter class for every `bind_via = "options_field"` inbound bridge
/// into the main module file (e.g. `HtmlToMarkdown.swift`).
///
/// For each such bridge this produces:
///
/// 1. `public protocol {Trait}Protocol: AnyObject` — user-facing protocol with `String`-typed
///    params and `VisitResult` return.  A default extension returns the result enum's first
///    unit variant (e.g. `` .`continue` ``) for all methods, so conformers only override callbacks
///    they care about.
/// 2. A private adapter class `_{Trait}ProtocolAdapter` that wraps the user protocol and
///    translates RustString params → String, serializes `VisitResult` → JSON String.
///    This implements the internal `Swift{Trait}BoxDelegate` protocol defined in RustBridge.
/// 3. A `public func make{TraitCamel}Handle(...)` factory that creates the opaque handle.
///
/// NOTE: `Swift{Trait}Box` (with RustString-typed methods) is emitted into `Sources/RustBridge/`
/// by `emit_inbound_box_files` because the swift-bridge @_cdecl shims reference it there.
fn emit_inbound_protocols(api: &ApiSurface, config: &ResolvedCrateConfig, out: &mut String) {
    for bridge_cfg in &config.trait_bridges {
        if bridge_cfg.bind_via != BridgeBinding::OptionsField {
            continue;
        }
        if bridge_cfg.exclude_languages.iter().any(|l| l == "swift") {
            continue;
        }
        let trait_name = &bridge_cfg.trait_name;
        let type_alias = match bridge_cfg.type_alias.as_deref() {
            Some(a) => a,
            None => continue,
        };
        let result_type_name = bridge_cfg.result_type.as_deref().unwrap_or("VisitResult");

        // Locate trait def in the API surface.
        let Some(trait_def) = api.types.iter().find(|t| t.is_trait && t.name == *trait_name) else {
            continue;
        };

        // Locate the result enum in the API surface for JSON serialization codegen.
        let result_enum = api.enums.iter().find(|e| e.name == result_type_name);

        let box_name = format!("Swift{trait_name}Box");
        let adapter_name = format!("_{trait_name}ProtocolAdapter");
        let protocol_name = format!("{trait_name}Protocol");
        // Match the underscore-prefixed name emitted in `Swift{Trait}Box.swift`.
        // The leading `_` signals "internal binding surface" without breaking
        // cross-module visibility that Swift requires for protocol conformance.
        let delegate_protocol_name = format!("_Swift{trait_name}BoxDelegate");
        let factory_fn = format!("make{}Handle", trait_name.to_upper_camel_case());

        // --- 1. Protocol definition (user-facing, String params, VisitResult return) ---
        out.push_str(&format!(
            "/// Swift protocol that Swift classes implement to provide visitor callbacks.\n\
             /// Conform to this protocol to intercept HTML\u{2192}Markdown conversion events.\n\
             public protocol {protocol_name}: AnyObject {{\n"
        ));
        for method in &trait_def.methods {
            let method_snake = method.name.to_snake_case();
            let method_camel = method_snake.to_lower_camel_case();
            let params = swift_protocol_params(method);
            out.push_str(&format!("    func {method_camel}({params}) -> {result_type_name}\n"));
        }
        out.push_str("}\n\n");

        // --- 2. Protocol default extension ---
        // Determine the default-return variant of the result enum: the first
        // unit (no-field) variant, emitted with backtick-escaped Swift case
        // ident to match the enum's actual `case` declaration (see `emit_enum`
        // / `emit_variant_with_data`, both of which use `swift_case_ident`).
        // Falls back to a backtick-escaped `continue` literal if the result
        // enum isn't in the API surface — preserving the historical behaviour
        // for the canonical `VisitResult::Continue` shape.
        let default_case = result_enum
            .and_then(|en| en.variants.iter().find(|v| v.fields.is_empty()))
            .map(|v| swift_case_ident(&v.name.to_lower_camel_case()))
            .unwrap_or_else(|| swift_case_ident("continue"));
        out.push_str(&format!(
            "/// Default implementation: every method returns `.{default_case}` so conforming\n\
             /// types only need to implement the callbacks they care about.\n\
             public extension {protocol_name} {{\n"
        ));
        for method in &trait_def.methods {
            let method_snake = method.name.to_snake_case();
            let method_camel = method_snake.to_lower_camel_case();
            let underscore_params = swift_protocol_underscore_params(method);
            out.push_str(&format!(
                "    func {method_camel}({underscore_params}) -> {result_type_name} {{ return .{default_case} }}\n"
            ));
        }
        out.push_str("}\n\n");

        // --- 3. Adapter class (private, lives in HtmlToMarkdown module) ---
        // Implements Swift{Trait}BoxDelegate (from RustBridge), wrapping any {Trait}Protocol.
        // Converts RustString → String, passes to user protocol, serializes result to JSON.
        out.push_str(&format!(
            "/// Internal adapter: wraps a `{protocol_name}` conformer as a `{delegate_protocol_name}`.\n\
             /// Converts swift-bridge raw types (RustString, UInt, etc.) to user-friendly Swift\n\
             /// types before dispatching, and serialises the `{result_type_name}` return to JSON.\n\
             private final class {adapter_name}: {delegate_protocol_name} {{\n\
             \x20   private let inner: any {protocol_name}\n\
             \x20   init(_ inner: any {protocol_name}) {{ self.inner = inner }}\n"
        ));
        // Emit one delegate method per trait method.
        for method in &trait_def.methods {
            let method_snake = method.name.to_snake_case();
            let method_camel = method_snake.to_lower_camel_case();
            let delegate_method = swift_ident(&method_camel);
            let delegate_params = swift_box_params(method); // RustString-typed
            let (conversion_lines, call_args) = swift_adapter_conversions(method);
            // Adapter implements the delegate protocol: method name must match the
            // protocol's lowerCamelCase declaration emitted in `emit_inbound_box_files`.
            out.push_str(&format!("    func {delegate_method}({delegate_params}) -> String {{\n"));
            for line in &conversion_lines {
                out.push_str(&format!("        {line}\n"));
            }
            let result_json = if let Some(_en) = result_enum {
                format!(
                    "        return {}_toJson(inner.{method_camel}({call_args}))\n",
                    result_type_name.to_snake_case()
                )
            } else {
                "        return \"{}\"\n".to_string()
            };
            out.push_str(&result_json);
            out.push_str("    }\n");
        }
        out.push_str("}\n\n");

        // --- 4. JSON serialization helper for the result type ---
        if let Some(en) = result_enum {
            let fn_name = format!("{}_toJson", result_type_name.to_snake_case());
            out.push_str(&format!(
                "/// Serialise a `{result_type_name}` to a JSON string matching Rust serde defaults.\n\
                 private func {fn_name}(_ result: {result_type_name}) -> String {{\n\
                 \x20   switch result {{\n"
            ));
            for variant in &en.variants {
                let variant_name = &variant.name;
                // The Swift case name: camelCase of variant name. Unit variants are bare;
                // newtype variants have associated values.
                // Must match the case name emitted by `emit_enum` /
                // `emit_variant_with_data`, hence backtick-escaped via
                // `swift_case_ident` rather than the Rust-side `swift_ident`.
                let swift_case = swift_case_ident(&variant_name.to_lower_camel_case());
                if variant.fields.is_empty() {
                    // Unit variant: serialises to `"VariantName"` (a JSON string).
                    out.push_str(&format!("    case .{swift_case}: return \"\\\"{}\\\"\"", variant_name));
                } else if variant.is_tuple && variant.fields.len() == 1 {
                    // Newtype variant: serialises to `{\"VariantName\":\"value\"}`.
                    let _field_swift = if variant.fields[0].name.starts_with("field") {
                        "field0".to_string()
                    } else {
                        variant.fields[0].name.to_lower_camel_case()
                    };
                    // The associated value label in Swift for a tuple variant is `field0:`.
                    out.push_str(&format!(
                        "    case .{swift_case}(let v): return \"{{\\\"{}\\\":\\\"\\(jsonEscapeStr(v))\\\"}}\"",
                        variant_name
                    ));
                }
                out.push('\n');
            }
            out.push_str("    }\n}\n\n");
            // Emit jsonEscapeStr helper once (idempotent — only once per bridge).
            out.push_str(
                "/// Escape a Swift String for embedding in a JSON string literal.\n\
                 private func jsonEscapeStr(_ s: String) -> String {\n\
                 \x20   s.replacingOccurrences(of: \"\\\\\", with: \"\\\\\\\\\")\n\
                 \x20    .replacingOccurrences(of: \"\\\"\", with: \"\\\\\\\"\")\n\
                 \x20    .replacingOccurrences(of: \"\\n\", with: \"\\\\n\")\n\
                 \x20    .replacingOccurrences(of: \"\\r\", with: \"\\\\r\")\n\
                 \x20    .replacingOccurrences(of: \"\\t\", with: \"\\\\t\")\n\
                 }\n\n",
            );
        }

        // --- 5. Factory function ---
        let options_type = bridge_cfg.options_type.as_deref().unwrap_or("ConversionOptions");
        let field = bridge_cfg.resolved_options_field().unwrap_or("visitor");
        let opts_snake = options_type.to_snake_case();
        let options_fn = format!("{opts_snake}FromJsonWith{}", field.to_upper_camel_case()).to_lower_camel_case();
        out.push_str(&format!(
            "/// Wrap a `{protocol_name}` conformer in an opaque `{type_alias}` handle\n\
             /// that can be passed to `{options_fn}(...)` on the Rust side.\n\
             public func {factory_fn}(_ visitor: any {protocol_name}) -> {type_alias} {{\n\
             \x20   return RustBridge.make{trait_name}Handle({box_name}({adapter_name}(visitor)))\n\
             }}\n\n",
        ));

        // --- 6. Top-level forwarder for `{options_type}FromJsonWith{Field}` ---
        // The Rust bridge emits this as a free function in the RustBridge module
        // (see `gen_rust_crate::plugin_inbound` and the matching swift-bridge
        // `swift_name = "{opts_camel}FromJsonWith{Field}"` attribute). Without a
        // top-level forwarder in the user-facing module, e2e tests and end-users
        // would have to call `RustBridge.{options_fn}(...)` directly, which (a)
        // is unidiomatic and (b) drags in `import RustBridge` which collides
        // with first-class Swift types (e.g. the `VisitResult` enum vs the
        // opaque `RustBridge.VisitResult` class).
        out.push_str(&format!(
            "/// Decode `{options_type}` JSON and attach a `{type_alias}` visitor handle.\n\
             /// Forwards to the swift-bridge shim emitted in the `RustBridge` module —\n\
             /// re-exposed here so callers do not need `import RustBridge`.\n\
             public func {options_fn}(_ json: String, _ {field}: {type_alias}?) throws -> {options_type} {{\n\
             \x20   return try RustBridge.{options_fn}(json, {field})\n\
             }}\n\n",
        ));
        let _ = api;
    }
}

// ---------------------------------------------------------------------------
// Free-function and trait-bridge top-level forwarders
// ---------------------------------------------------------------------------

/// Returns the set of public-func names already emitted in `Kreuzberg.swift` by
/// earlier emission passes. Forwarders for `api.functions` entries that would
/// produce an identical Swift function name are skipped to avoid duplicate
/// declarations.
fn already_emitted_top_level_names(api: &ApiSurface) -> std::collections::HashSet<String> {
    let mut names: std::collections::HashSet<String> = std::collections::HashSet::new();

    // Bytes/path convenience wrappers (`emit_bytes_overloads` / `emit_path_overload`).
    // The wrapper name strips a trailing `Sync` from the camelCased function name;
    // emission is skipped entirely when that strip is a no-op, so we mirror the
    // same guard here.
    for func in &api.functions {
        if func.is_async {
            continue;
        }
        let first = func.params.first().map(|p| &p.ty);
        let is_bytes_or_path = matches!(first, Some(TypeRef::Bytes) | Some(TypeRef::Path));
        if !is_bytes_or_path {
            continue;
        }
        if convenience_name_shadows_bridge(func) {
            continue;
        }
        let swift_inner = swift_ident(&func.name.to_lower_camel_case());
        let wrapper_name = if swift_inner.ends_with("Sync") {
            swift_inner[..swift_inner.len() - 4].to_string()
        } else {
            swift_inner
        };
        names.insert(wrapper_name);
    }

    names
}

/// Emit a top-level `public func` forwarder for every entry in `api.functions`
/// that is not already covered by an earlier emission pass.
///
/// The forwarder uses native Swift types for the public signature
/// (`String`, `[UInt8]`, `[String]`, primitives, opaque types, …) and converts
/// to the swift-bridge runtime types (`RustString`, `RustVec<UInt8>`,
/// `RustVec<RustString>`) inside the body before calling the bridge function in
/// the `RustBridge` module.
fn emit_free_function_forwarders(
    api: &ApiSurface,
    config: &ResolvedCrateConfig,
    known_dto_names: &std::collections::HashSet<String>,
    client_class_names: &std::collections::HashSet<String>,
    out: &mut String,
) {
    let exclude_functions: std::collections::HashSet<String> = config
        .swift
        .as_ref()
        .map(|c| c.exclude_functions.iter().cloned().collect())
        .unwrap_or_default();

    let already = already_emitted_top_level_names(api);

    // Collect forwarders so we can decide whether to emit the section header at all.
    let mut emitted_any = false;

    for func in &api.functions {
        if func.binding_excluded {
            continue;
        }
        if exclude_functions.contains(&func.name) {
            continue;
        }
        // Skip trait-bridge-managed names (clear_fn) — the trait-bridge forwarder
        // emission pass below emits its own throwing wrapper, and duplicating it
        // here would cause Swift `invalid redeclaration` at the userland layer.
        if crate::codegen::generators::trait_bridge::is_trait_bridge_managed_fn(&func.name, &config.trait_bridges) {
            continue;
        }
        let swift_name = swift_ident(&func.name.to_lower_camel_case());
        if already.contains(&swift_name) {
            continue;
        }
        if !emitted_any {
            out.push_str("// MARK: - Free-function Forwarders\n");
            out.push_str(
                "// Re-export every public free function on the source Rust crate as a\n\
                 // top-level `public func` on the host module so consumers do not need to\n\
                 // `import RustBridge` directly. Forwarders take Swift-native parameter\n\
                 // types and convert to the swift-bridge runtime types internally.\n\n",
            );
            emitted_any = true;
        }
        if func.is_async {
            emit_async_free_function_forwarder(func, &swift_name, known_dto_names, out);
        } else {
            emit_single_free_function_forwarder(func, &swift_name, known_dto_names, client_class_names, out);
        }
    }
}

/// Emit a single `public func` forwarder that delegates to `RustBridge.{swift_name}`.
fn emit_single_free_function_forwarder(
    func: &FunctionDef,
    swift_name: &str,
    known_dto_names: &std::collections::HashSet<String>,
    client_class_names: &std::collections::HashSet<String>,
    out: &mut String,
) {
    // `throws_clause` widens to `throws` when the function itself returns a
    // `Result<_, _>`, when the return-value conversion can throw (Named DTO
    // conversions go through `init(_ rb: RustBridge.{Name}) throws`), or when
    // any parameter conversion can throw (Named DTO params go through
    // `intoRust() throws`).
    let return_conversion_throws = return_value_conversion_throws(&func.return_type, known_dto_names);
    let any_param_throws = func
        .params
        .iter()
        .any(|p| param_conversion_throws(&p.ty, known_dto_names));
    let throws_clause = if func.error_type.is_some() || return_conversion_throws || any_param_throws {
        " throws"
    } else {
        ""
    };
    let try_keyword = if func.error_type.is_some() { "try " } else { "" };
    let return_ty = forwarder_return_type(&func.return_type);
    let return_clause = if matches!(&func.return_type, TypeRef::Unit) {
        String::new()
    } else {
        format!(" -> {return_ty}")
    };

    // Signature: collect each param as `name: SwiftType`. We always use labeled
    // params (matching the user-facing API) — the inner bridge call uses positional
    // arguments because swift-bridge bridge functions expose unlabeled first params.
    let mut sig_params: Vec<String> = Vec::with_capacity(func.params.len());
    let mut conversion_lines: Vec<String> = Vec::new();
    let mut call_args: Vec<String> = Vec::with_capacity(func.params.len());

    for param in &func.params {
        let swift_param_name = swift_ident(&param.name.to_lower_camel_case());
        let (swift_ty, local_expr) =
            forwarder_param_signature(&param.ty, &swift_param_name, param.optional, known_dto_names);
        sig_params.push(format!("{swift_param_name}: {swift_ty}"));
        if let Some(line) = local_expr.setup_line.clone() {
            conversion_lines.push(line);
        }
        call_args.push(local_expr.arg_expr);
    }
    let sig = sig_params.join(", ");
    let args = call_args.join(", ");

    if !func.doc.is_empty() {
        emit_doc_comment(&func.doc, "", out);
    }
    out.push_str(&format!(
        "public func {swift_name}({sig}){throws_clause}{return_clause} {{\n"
    ));
    for line in &conversion_lines {
        out.push_str(&format!("    {line}\n"));
    }
    let return_suffix =
        forwarder_return_conversion_suffix_with_throws(&func.return_type, known_dto_names, func.error_type.is_some());
    // The outer return expression must use `try` when either the bridge call
    // throws (`func.error_type.is_some()`) or the value-conversion suffix
    // contains a throwing call (Named DTO via `init(_ rb:) throws`).
    let effective_try = if func.error_type.is_some() || return_conversion_throws {
        "try "
    } else {
        ""
    };
    let _ = try_keyword; // superseded by `effective_try`

    // swift-bridge cannot natively bridge deeply-nested Vec (Vec<Vec<T>>) or
    // Map across the throws boundary — it serializes to RustString via serde
    // and exposes the function as `throws -> RustString`. Detect those cases
    // and emit a JSON decode body instead of the single-expression return.
    if func.error_type.is_some() && return_uses_json_bridge(&func.return_type) {
        let decode_ty = forwarder_return_type(&func.return_type);
        out.push_str(&format!(
            "    let _rb_json = try RustBridge.{swift_name}({args}).toString()\n"
        ));
        out.push_str("    let _rb_data = _rb_json.data(using: .utf8) ?? Data()\n");
        out.push_str(&format!(
            "    return try JSONDecoder().decode({decode_ty}.self, from: _rb_data)\n"
        ));
    } else if non_throwing_optional_string_uses_json_bridge(&func.return_type, func.error_type.is_some()) {
        // swift-bridge JSON-serializes ALL `Option<T>` returns at the bridge boundary
        // (see `needs_json_bridge` in `gen_rust_crate/type_bridge.rs`: `Optional(_) => true`).
        // For non-throwing `Option<String>` the bridge function is declared
        // `-> String` and surfaced in Swift as `-> RustString` (non-optional). The
        // forwarder declares `-> String?` from the IR. The v0.17.10 attempt to use
        // `?.toString()` was a type error (can't optional-chain on non-optional
        // RustString). Decode the JSON payload as `String?.self` instead — `nil`
        // becomes JSON `null` which decodes to `Optional<String>.none`, and a
        // present value decodes to `.some(value)`.
        let decode_ty = forwarder_return_type(&func.return_type);
        out.push_str(&format!(
            "    let _rb_json = RustBridge.{swift_name}({args}).toString()\n"
        ));
        out.push_str("    let _rb_data = _rb_json.data(using: .utf8) ?? Data()\n");
        out.push_str(&format!(
            "    return (try? JSONDecoder().decode({decode_ty}.self, from: _rb_data)) ?? nil\n"
        ));
    } else if matches!(&func.return_type, TypeRef::Named(n) if client_class_names.contains(n)) {
        // Bare Named return that is a Swift client class (declared in
        // `[crates.swift.client_constructor_body]`). The bridge call yields a
        // `RustBridge.{Class}` value; the forwarder signature declares the
        // higher-level `{Class}` wrapper. Use the internal init emitted by
        // `emit_client_class` to wrap the bridge handle.
        let bridge_call_try = if func.error_type.is_some() { "try " } else { "" };
        let class_name = swift_type_name(&func.return_type);
        out.push_str(&format!(
            "    let _rb = {bridge_call_try}RustBridge.{swift_name}({args})\n"
        ));
        out.push_str(&format!("    return {class_name}(_rb)\n"));
    } else if bare_named_dto_return(&func.return_type, known_dto_names) {
        // Bare Named DTO return: the bridge call yields a `RustBridge.{Dto}` value,
        // but the forwarder signature declares the high-level `{Dto}` struct/enum
        // (Codable/Sendable). There is no chainable conversion expression for the
        // `init(_ rb:) throws` initializer on a non-Optional/non-Sequence value, so
        // we bind the bridge result to a local and call the initializer separately.
        // This covers nullary default-config getters like `schemaQueryOnly()` →
        // `QueryOnlyConfig` whose body would otherwise return the low-level
        // `RustBridge.QueryOnlyConfig` and fail to satisfy the declared type.
        let bridge_call_try = if func.error_type.is_some() { "try " } else { "" };
        let dto_name = swift_type_name(&func.return_type);
        out.push_str(&format!(
            "    let _rb = {bridge_call_try}RustBridge.{swift_name}({args})\n"
        ));
        out.push_str(&format!("    return try {dto_name}(_rb)\n"));
    } else {
        out.push_str(&format!(
            "    return {effective_try}RustBridge.{swift_name}({args}){return_suffix}\n"
        ));
    }
    out.push_str("}\n\n");
}

/// Emit an async `public func` forwarder for an async function.
fn emit_async_free_function_forwarder(
    func: &FunctionDef,
    swift_name: &str,
    known_dto_names: &std::collections::HashSet<String>,
    out: &mut String,
) {
    let return_conversion_throws = return_value_conversion_throws(&func.return_type, known_dto_names);
    let any_param_throws = func
        .params
        .iter()
        .any(|p| param_conversion_throws(&p.ty, known_dto_names));
    let throws_clause = if func.error_type.is_some() || return_conversion_throws || any_param_throws {
        " throws"
    } else {
        ""
    };
    let return_ty = forwarder_return_type(&func.return_type);
    let return_clause = if matches!(&func.return_type, TypeRef::Unit) {
        String::new()
    } else {
        format!(" -> {return_ty}")
    };

    // Note: We used to only wrap String parameters when there was a Vec<Named(DTO)> parameter.
    // Now we always wrap String parameters to ensure type inference works correctly inside
    // the Task.detached closure, where generic inference is weaker.

    // Signature: collect each param as `name: SwiftType`.
    let mut sig_params: Vec<String> = Vec::with_capacity(func.params.len());
    let mut conversion_lines: Vec<String> = Vec::new();
    let mut call_args: Vec<String> = Vec::with_capacity(func.params.len());

    for param in &func.params {
        let swift_param_name = swift_ident(&param.name.to_lower_camel_case());
        let (swift_ty, local_expr) =
            forwarder_param_signature(&param.ty, &swift_param_name, param.optional, known_dto_names);
        sig_params.push(format!("{swift_param_name}: {swift_ty}"));
        if let Some(line) = local_expr.setup_line.clone() {
            conversion_lines.push(line);
        }
        // When we have a Vec<DTO> parameter, the generic type is constrained to RustString,
        // so we must wrap all String parameters in RustString() for type compatibility.
        // Additionally, wrap ANY String parameter to give Swift's type inference a hand
        // inside the Task.detached closure, where generic inference is weaker.
        let arg_expr = if matches!(&param.ty, TypeRef::String) && !param.optional {
            format!("RustString({swift_param_name})")
        } else {
            local_expr.arg_expr
        };
        call_args.push(arg_expr);
    }
    let sig = sig_params.join(", ");
    let args = call_args.join(", ");

    if !func.doc.is_empty() {
        emit_doc_comment(&func.doc, "", out);
    }
    out.push_str(&format!(
        "public func {swift_name}({sig}) async{throws_clause}{return_clause} {{\n"
    ));
    for line in &conversion_lines {
        out.push_str(&format!("    {line}\n"));
    }

    let effective_try = if func.error_type.is_some() || return_conversion_throws {
        "try "
    } else {
        ""
    };

    // Call the synchronous bridge function in a Task.detached to avoid blocking the async context.
    // swift-bridge 0.1.x cannot natively bridge async Rust functions, so async functions are
    // declared in the extern block as blocking sync functions. We wrap the blocking call in
    // Task.detached to prevent blocking the caller's async context.
    //
    // For first-class DTOs, convert RustBridge.T -> T inside the closure so Swift's type
    // inference can correctly determine the closure's return type.
    let (bridge_call, return_stmt) = match &func.return_type {
        TypeRef::Named(name) if known_dto_names.contains(name) => {
            let struct_name = swift_ident(name);
            (
                format!("try RustBridge.{swift_name}({args})"),
                format!("        return try {struct_name}(_rb_obj)"),
            )
        }
        _ => (
            format!("try RustBridge.{swift_name}({args})"),
            "        return result".to_string(),
        ),
    };

    out.push_str(&format!(
        "    return {effective_try}await Task.detached(priority: .userInitiated) {{\n"
    ));

    if matches!(&func.return_type, TypeRef::Named(name) if known_dto_names.contains(name)) {
        out.push_str(&format!("        let _rb_obj = {bridge_call}\n"));
        out.push_str(&format!("{return_stmt}\n"));
    } else {
        out.push_str(&format!("        let result = {bridge_call}\n"));
        out.push_str(&format!("{return_stmt}\n"));
    }

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

/// Returns `true` when `ty` is a bare `Named(name)` reference whose Swift mirror
/// is a known first-class DTO (i.e. has an `init(_ rb:) throws` converter). Bare
/// in the sense of not wrapped in `Optional` / `Vec` / `Map` / etc. — those have
/// their own chainable suffixes via [`forwarder_return_conversion_suffix_inner`].
fn bare_named_dto_return(ty: &TypeRef, known_dto_names: &std::collections::HashSet<String>) -> bool {
    matches!(ty, TypeRef::Named(name) if known_dto_names.contains(name))
}

/// True if swift-bridge transports the function's return value through a
/// JSON-serialised `RustString` instead of a native bridge type. Applies only
/// to `throws` functions whose return type is too complex for swift-bridge's
/// built-in `RustVec<T>` / `RustString` / primitive mappings — specifically
/// nested `Vec<Vec<_>>`, `Map<_, _>`, and `Json`.
fn return_uses_json_bridge(ty: &TypeRef) -> bool {
    match ty {
        TypeRef::Vec(inner) => matches!(inner.as_ref(), TypeRef::Vec(_) | TypeRef::Map(_, _)),
        TypeRef::Map(_, _) | TypeRef::Json => true,
        TypeRef::Optional(inner) => return_uses_json_bridge(inner),
        _ => false,
    }
}

/// True if `ty` is `Option<String>` or `Option<Primitive>` on a non-throwing
/// function, where swift-bridge JSON-serializes the value through a
/// non-optional `RustString` bridge return.
///
/// Per `needs_json_bridge` in `gen_rust_crate/type_bridge.rs`, ALL `Option<T>`
/// returns are JSON-bridged (line 100: `TypeRef::Optional(_) => true`). For a
/// non-throwing function the bridge declares `-> String` (the JSON payload),
/// which swift-bridge surfaces in Swift as `-> RustString` (non-optional). The
/// forwarder must JSON-decode rather than apply a chained `?.toString()` — the
/// latter is a type error against the non-optional `RustString`.
///
/// Covers `Option<String>` (sample_core parser-pack `detect_language_from_extension`)
/// and `Option<Primitive>` (sample-llm `completion_cost` returning
/// `Option<f64>`). Other shapes (`Option<Named>`, `Option<Vec<…>>`) hit the
/// same bridge but the JSON payload decodes to a Codable target — extend the
/// match arms as new shapes surface.
fn non_throwing_optional_string_uses_json_bridge(ty: &TypeRef, throws: bool) -> bool {
    if throws {
        return false;
    }
    matches!(
        ty,
        TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::String | TypeRef::Primitive(_))
    )
}

/// Capture both the inline argument expression and the optional setup line
/// required to materialise a swift-bridge-compatible value from a Swift-native
/// parameter.
struct ForwarderArg {
    /// Optional `let <local> = <expr>` statement that must run before the call.
    setup_line: Option<String>,
    /// The expression passed positionally into the `RustBridge.fn(...)` call.
    arg_expr: String,
}

/// Map a `TypeRef` to the Swift type used in the public forwarder signature, and
/// produce the (optional) conversion line + call argument used to pass it to the
/// swift-bridge runtime.
///
/// The pairings are intentionally narrow: only conversions used by sample_core's
/// public free functions are materialised. Anything not handled defaults to
/// passing the Swift value through unchanged (sufficient for `String`,
/// primitives, opaque `Named` types).
fn forwarder_param_signature(
    ty: &TypeRef,
    swift_param_name: &str,
    optional: bool,
    known_dto_names: &std::collections::HashSet<String>,
) -> (String, ForwarderArg) {
    let inner_ty = if optional {
        if let TypeRef::Optional(inner) = ty {
            inner.as_ref().clone()
        } else {
            ty.clone()
        }
    } else {
        ty.clone()
    };
    let make_optional = |inner: &str| -> String {
        if optional || matches!(ty, TypeRef::Optional(_)) {
            format!("{inner}?")
        } else {
            inner.to_string()
        }
    };
    match &inner_ty {
        TypeRef::Bytes => {
            let swift_ty = make_optional("[UInt8]");
            let local = format!("_rb_{swift_param_name}");
            let setup = if optional || matches!(ty, TypeRef::Optional(_)) {
                Some(format!(
                    "let {local} = {swift_param_name}.map {{ bytes -> RustVec<UInt8> in let v = RustVec<UInt8>(); for b in bytes {{ v.push(value: b) }}; return v }}"
                ))
            } else {
                Some(format!(
                    "let {local}: RustVec<UInt8> = {{ let v = RustVec<UInt8>(); for b in {swift_param_name} {{ v.push(value: b) }}; return v }}()"
                ))
            };
            (
                swift_ty,
                ForwarderArg {
                    setup_line: setup,
                    arg_expr: local,
                },
            )
        }
        TypeRef::Vec(elem) => match elem.as_ref() {
            TypeRef::String => {
                let swift_ty = make_optional("[String]");
                let local = format!("_rb_{swift_param_name}");
                let setup = if optional || matches!(ty, TypeRef::Optional(_)) {
                    Some(format!(
                        "let {local} = {swift_param_name}.map {{ strs -> RustVec<RustString> in let v = RustVec<RustString>(); for s in strs {{ v.push(value: RustString(s)) }}; return v }}"
                    ))
                } else {
                    Some(format!(
                        "let {local}: RustVec<RustString> = {{ let v = RustVec<RustString>(); for s in {swift_param_name} {{ v.push(value: RustString(s)) }}; return v }}()"
                    ))
                };
                (
                    swift_ty,
                    ForwarderArg {
                        setup_line: setup,
                        arg_expr: local,
                    },
                )
            }
            TypeRef::Primitive(_) => {
                let inner = swift_type_name(elem);
                let swift_ty = make_optional(&format!("[{inner}]"));
                let local = format!("_rb_{swift_param_name}");
                let setup = if optional || matches!(ty, TypeRef::Optional(_)) {
                    Some(format!(
                        "let {local} = {swift_param_name}.map {{ xs -> RustVec<{inner}> in let v = RustVec<{inner}>(); for x in xs {{ v.push(value: x) }}; return v }}"
                    ))
                } else {
                    Some(format!(
                        "let {local}: RustVec<{inner}> = {{ let v = RustVec<{inner}>(); for x in {swift_param_name} {{ v.push(value: x) }}; return v }}()"
                    ))
                };
                (
                    swift_ty,
                    ForwarderArg {
                        setup_line: setup,
                        arg_expr: local,
                    },
                )
            }
            TypeRef::Named(name) if known_dto_names.contains(name) => {
                // Vec<FirstClassDTO>: encode each element to JSON and build RustVec<RustString>.
                // Each DTO has `Codable` + `encode(to:)`, so we serialize to JSON strings.
                let swift_ty = make_optional(&format!("[{name}]"));
                let local = format!("_rb_{swift_param_name}");
                let setup = if optional || matches!(ty, TypeRef::Optional(_)) {
                    Some(format!(
                        "let {local} = try {swift_param_name}.map {{ items -> RustVec<RustString> in let v = RustVec<RustString>(); for item in items {{ let data = try JSONEncoder().encode(item); let json = String(data: data, encoding: .utf8) ?? \"null\"; v.push(value: RustString(json)) }}; return v }}"
                    ))
                } else {
                    Some(format!(
                        "let {local}: RustVec<RustString> = try ({{ () throws -> RustVec<RustString> in let v = RustVec<RustString>(); for item in {swift_param_name} {{ let data = try JSONEncoder().encode(item); let json = String(data: data, encoding: .utf8) ?? \"null\"; v.push(value: RustString(json)) }}; return v }}())"
                    ))
                };
                (
                    swift_ty,
                    ForwarderArg {
                        setup_line: setup,
                        arg_expr: local,
                    },
                )
            }
            _ => {
                // Fall back to passing the Swift-side array directly; swift-bridge
                // accepts `RustVec<T>` for opaque T but sample_core does not currently
                // expose such free functions, so the simpler passthrough is fine.
                let swift_ty = make_optional(&swift_type_name(ty));
                (
                    swift_ty,
                    ForwarderArg {
                        setup_line: None,
                        arg_expr: swift_param_name.to_string(),
                    },
                )
            }
        },
        // Host-facing Named DTO params: the public forwarder accepts the
        // high-level Swift struct (Codable/Sendable), but the swift-bridge
        // `RustBridge.{fn}` extern declares the low-level `RustBridge.{Dto}`
        // class. Bridge the gap with the symmetric `intoRust()` initializer
        // that every first-class DTO emits (see `emit_into_rust_direct_call`).
        // Without this conversion Swift rejects the call with "cannot convert
        // value of type '{Module}.{Dto}' to expected argument type
        // 'RustBridge.{Dto}'".
        TypeRef::Named(name) if known_dto_names.contains(name) => {
            let swift_ty = make_optional(&swift_type_name(&inner_ty));
            let local = format!("_rb_{swift_param_name}");
            // `intoRust()` is `throws -> RustBridge.{Dto}`. The Optional
            // overload uses optional-chained call which still propagates the
            // throw out of the surrounding throwing context.
            let setup = if optional || matches!(ty, TypeRef::Optional(_)) {
                Some(format!("let {local} = try {swift_param_name}?.intoRust()"))
            } else {
                Some(format!("let {local} = try {swift_param_name}.intoRust()"))
            };
            (
                swift_ty,
                ForwarderArg {
                    setup_line: setup,
                    arg_expr: local,
                },
            )
        }
        _ => {
            let swift_ty = make_optional(&swift_type_name(&inner_ty));
            (
                swift_ty,
                ForwarderArg {
                    setup_line: None,
                    arg_expr: swift_param_name.to_string(),
                },
            )
        }
    }
}

/// Returns true when emitting the forwarder argument expression for `ty` requires
/// a throwing context. Mirrors the conversion logic in
/// [`forwarder_param_signature`]: Named-DTO params (bare or `Optional`) invoke
/// the throwing `intoRust()` initializer, and `Vec<Named(DTO)>` encodes each
/// element to JSON via throwing `JSONEncoder`.
fn param_conversion_throws(ty: &TypeRef, known_dto_names: &std::collections::HashSet<String>) -> bool {
    match ty {
        TypeRef::Named(name) => known_dto_names.contains(name),
        TypeRef::Optional(inner) => matches!(
            inner.as_ref(),
            TypeRef::Named(name) if known_dto_names.contains(name)
        ),
        TypeRef::Vec(elem) => matches!(
            elem.as_ref(),
            TypeRef::Named(name) if known_dto_names.contains(name)
        ),
        _ => false,
    }
}

/// Map a free function return `TypeRef` to the Swift type used in the forwarder
/// signature, converting swift-bridge runtime types to native Swift collections.
fn forwarder_return_type(ty: &TypeRef) -> String {
    match ty {
        TypeRef::String => "String".to_string(),
        TypeRef::Bytes => "[UInt8]".to_string(),
        TypeRef::Vec(inner) => format!("[{}]", forwarder_return_type(inner)),
        TypeRef::Optional(inner) => format!("{}?", forwarder_return_type(inner)),
        _ => swift_type_name(ty),
    }
}

/// Conversion suffix applied to the `RustBridge.fnName(...)` call so the
/// returned value matches the native Swift return type emitted by
/// [`forwarder_return_type`].
///
/// swift-bridge maps a bare `String` (and `Result<String, _>`) return directly
/// to a Swift-native `String` — appending `.toString()` is a compile error
/// against the native type (see `bytes_overload_with_string_return_does_not_append_to_string`).
/// For `RustVec<T>` returns the convention is `.map { $0 }` to materialise as a
/// Swift `Array`; the closure receives the leaf swift-bridge type which can be
/// converted further if needed.
fn forwarder_return_conversion_suffix_with_throws(
    ty: &TypeRef,
    known_dto_names: &std::collections::HashSet<String>,
    throws: bool,
) -> String {
    forwarder_return_conversion_suffix_inner(ty, known_dto_names, throws)
}

fn forwarder_return_conversion_suffix_inner(
    ty: &TypeRef,
    known_dto_names: &std::collections::HashSet<String>,
    throws: bool,
) -> String {
    match ty {
        // swift-bridge transports the value through `RustString` whenever the
        // Rust function returns `Result<T, E>` (`throws` from Swift's POV), so
        // a bare `String` return still arrives as `RustString` and needs
        // `.toString()` to coerce to the native Swift `String` declared on the
        // forwarder signature. Non-throwing functions map `String` directly to
        // Swift-native `String` — no conversion needed (pinned by
        // `bytes_overload_with_string_return_does_not_append_to_string`).
        TypeRef::String if throws => ".toString()".to_string(),
        TypeRef::Bytes => ".map { $0 }".to_string(),
        TypeRef::Vec(inner) => match inner.as_ref() {
            // RustVec<RustString> iteration yields `RustStringRef` (borrowed).
            // We must NOT add a `RustStringRef.toString()` shim because
            // `RustString` is a subclass of `RustStringRef` and declares its
            // own `toString()` — Swift rejects this with "non-'@objc' instance
            // method 'toString()' is declared in extension of 'RustStringRef'
            // and cannot be overridden". Use the longer but type-safe
            // `as_str().toString()` chain directly at every call site.
            TypeRef::String => ".map { $0.as_str().toString() }".to_string(),
            TypeRef::Primitive(_) => ".map { $0 }".to_string(),
            // RustVec<RustBridge.{Dto}> iteration yields `RustBridge.{Dto}Ref` (borrowed)
            // — there is no symmetric `init(_ rb: {Dto}Ref)` for either first-class DTOs
            // or typealiased opaque classes. swift-bridge's `RustVec<T>.get(_:)` also
            // returns `T.SelfRef`, so producing an `[{Dto}]` from a `RustVec<{Dto}>` is
            // impossible inside a forwarder without a per-element JSON roundtrip. Until
            // we wire `Vec<Named>` through `needs_json_bridge`, the forwarder leaves the
            // expression unconverted (compile error surfaces loudly rather than silent
            // misroute). Consumers should exclude the function via
            // `[crates.swift.exclude_functions]` or add a hand-written adapter.
            TypeRef::Named(_) => String::new(),
            _ => String::new(),
        },
        // Optional<Named DTO> return: bridge call returns the low-level
        // `RustBridge.{Dto}?` class, but the forwarder signature is the
        // high-level `{Dto}?` struct (Codable/Sendable). Convert with the
        // first-class struct's `init(_ rb:) throws` initializer.
        TypeRef::Optional(inner) => match inner.as_ref() {
            // Optional<RustBridge.{Dto}> → Optional<{Dto}> via Optional.map's
            // `(Wrapped) -> U` transform. `init(_ rb:) throws` returns the owned
            // struct on success; failure propagates because the outer closure is
            // marked `try`.
            TypeRef::Named(name) if known_dto_names.contains(name) => {
                format!(".map {{ try {name}($0) }}")
            }
            // swift-bridge JSON-serializes `Option<String>` returns at the bridge
            // boundary (`needs_json_bridge` covers all Optionals): the bridge
            // declares `-> String` for non-throwing functions and surfaces in Swift
            // as a NON-optional `RustString`. The v0.17.10 attempt to apply
            // `?.toString()` here was a type error because the receiver is not
            // optional. The body-replacement branch in
            // [`emit_single_free_function_forwarder`]
            // (`non_throwing_optional_string_uses_json_bridge`) takes over for
            // non-throwing functions and emits a JSON-decode body instead, so the
            // suffix path is unused — emit an empty suffix to leave the body
            // branch in sole control.
            TypeRef::String => String::new(),
            _ => String::new(),
        },
        // Bare Named DTO return is currently emitted via DTO-aware free-function
        // bodies; the generic forwarder doesn't have a chainable suffix that can
        // wrap a bare class value into `try {Name}(value)` because there's no `.map`
        // entry point on a non-Optional/non-Sequence value. If sample_core ever
        // adds such a free function the forwarder body itself will need to
        // change shape (binding the result to a local first). Until then,
        // emit an empty suffix and let the type check fail loudly rather than
        // silently misroute.
        _ => String::new(),
    }
}

/// Returns true if the value-conversion suffix produced by
/// [`forwarder_return_conversion_suffix_inner`] contains a `try` expression — meaning
/// the surrounding `return ...` line must itself sit inside a throwing context.
///
/// Only Named-DTO conversions throw (`init(_ rb:) throws`). All other suffixes
/// are pure expressions that never fail.
fn return_value_conversion_throws(ty: &TypeRef, known_dto_names: &std::collections::HashSet<String>) -> bool {
    match ty {
        // `Optional<Named-DTO>` returns emit a throwing suffix
        // (`Optional.map { try {Name}($0) }`). Bare `Named(DTO)` returns route
        // through the local-binding body form (`let _rb = ...; return try
        // {Name}(_rb)`) which is also throwing. `Vec<Named>` returns emit no
        // suffix today — see the corresponding arms in
        // [`forwarder_return_conversion_suffix_inner`] and
        // [`bare_named_dto_return`].
        TypeRef::Optional(inner) => matches!(
            inner.as_ref(),
            TypeRef::Named(name) if known_dto_names.contains(name)
        ),
        TypeRef::Named(name) => known_dto_names.contains(name),
        _ => false,
    }
}

/// Emit top-level public forwarders for trait-bridge register/unregister/clear
/// entry points (`bind_via = "function_param"` bridges).
///
/// The Rust-side wrappers are produced by
/// `gen_rust_crate::plugin_inbound::emit_extern_block_for_inbound_registration`
/// and by the `TraitBridgeGenerator` impl in `gen_rust_crate::trait_bridge`.
/// swift-bridge re-exports them in the `RustBridge` module; without these
/// top-level forwarders consumers of `Kreuzberg` cannot reach the plugin
/// registration surface without an explicit `import RustBridge`.
///
/// Each bridge contributes up to three forwarders:
///   - `public func {register_fn}(_ swiftBox: Swift{Trait}Box) throws`
///   - `public func {unregister_fn}(_ name: String) throws`
///   - `public func {clear_fn}() throws`
fn emit_trait_bridge_forwarders(config: &ResolvedCrateConfig, out: &mut String) {
    let mut emitted_any = false;
    for bridge_cfg in &config.trait_bridges {
        if bridge_cfg.bind_via != BridgeBinding::FunctionParam {
            continue;
        }
        if bridge_cfg.exclude_languages.iter().any(|l| l == "swift") {
            continue;
        }
        if bridge_cfg.register_fn.is_none() && bridge_cfg.unregister_fn.is_none() && bridge_cfg.clear_fn.is_none() {
            continue;
        }
        if !emitted_any {
            out.push_str("// MARK: - Trait Bridge Registration Forwarders\n");
            out.push_str(
                "// Top-level `public func` re-exports of the swift-bridge–generated\n\
                 // `register_*` / `unregister_*` / `clear_*` plugin registration entry\n\
                 // points so consumers do not need to `import RustBridge` for plugin work.\n\n",
            );
            emitted_any = true;
        }
        let trait_name = &bridge_cfg.trait_name;
        let box_type = format!("Swift{trait_name}Box");

        if let Some(register_fn) = bridge_cfg.register_fn.as_deref() {
            let camel = register_fn.to_lower_camel_case();
            out.push_str(&format!(
                "/// Register an inbound `{trait_name}` plugin implementation. The Swift\n\
                 /// host wraps a `{trait_name}` conformer in a `{box_type}` adapter\n\
                 /// (see `Sources/RustBridge/Plugins.swift`); pass the wrapped instance to\n\
                 /// register the plugin in the global registry.\n\
                 public func {camel}(_ swiftBox: {box_type}) throws {{\n\
                 \x20   try RustBridge.{camel}(swiftBox)\n\
                 }}\n\n"
            ));
        }
        if let Some(unregister_fn) = bridge_cfg.unregister_fn.as_deref() {
            let camel = unregister_fn.to_lower_camel_case();
            out.push_str(&format!(
                "/// Unregister a previously-registered `{trait_name}` plugin by name.\n\
                 public func {camel}(_ name: String) throws {{\n\
                 \x20   try RustBridge.{camel}(name)\n\
                 }}\n\n"
            ));
        }
        if let Some(clear_fn) = bridge_cfg.clear_fn.as_deref() {
            let camel = clear_fn.to_lower_camel_case();
            out.push_str(&format!(
                "/// Remove every registered `{trait_name}` plugin. Typically used in test teardown.\n\
                 public func {camel}() throws {{\n\
                 \x20   try RustBridge.{camel}()\n\
                 }}\n\n"
            ));
        }
    }
}

/// Generate `Swift{Trait}Box.swift` files for every OptionsField bridge.
/// These go into `Sources/RustBridge/` so the swift-bridge @_cdecl shims can reference them.
///
/// Each file contains:
/// 1. `public protocol Swift{Trait}BoxDelegate: AnyObject` — internal-ish protocol with
///    RustString-typed params and String return (matching the @_cdecl shim call sites).
/// 2. `public final class Swift{Trait}Box` — the opaque type declared in `extern "Swift"`.
///    Methods have keyword labels and RustString params, returning String (JSON).
fn emit_inbound_box_files(
    api: &ApiSurface,
    config: &ResolvedCrateConfig,
    rust_bridge_dir: &std::path::Path,
) -> Vec<GeneratedFile> {
    let mut files = Vec::new();
    for bridge_cfg in &config.trait_bridges {
        if bridge_cfg.bind_via != BridgeBinding::OptionsField {
            continue;
        }
        if bridge_cfg.exclude_languages.iter().any(|l| l == "swift") {
            continue;
        }
        let trait_name = &bridge_cfg.trait_name;

        let Some(trait_def) = api.types.iter().find(|t| t.is_trait && t.name == *trait_name) else {
            continue;
        };

        // The box class name must remain `Swift{trait_name}Box` because swift-bridge's
        // `@_cdecl` shims reference it by exact name. The delegate protocol is named
        // with a leading underscore (`_SwiftHtmlVisitorBoxDelegate`) to signal that
        // it is an internal binding-detail surface even though Swift's access-control
        // rules force it to be `public` (it is implemented from a sibling SwiftPM target).
        let box_name = format!("Swift{trait_name}Box");
        let delegate_protocol_name = format!("_Swift{trait_name}BoxDelegate");

        let mut content = String::new();
        content.push_str("// Generated by alef. Do not edit by hand.\n");
        // swift-format-ignore-file: this file contains generated FFI glue that
        // violates swift-format's defaults (long lines, specific naming patterns).
        // Marking it here prevents the pre-commit swift-format hook from rewriting
        // the file on every regen and creating spurious diffs.
        content.push_str("// swift-format-ignore-file\n");
        content.push_str("// This file is in Sources/RustBridge/ because the swift-bridge @_cdecl\n");
        content.push_str("// shims reference Swift");
        content.push_str(trait_name);
        content.push_str("Box by name and must see it in the same module.\n\n");
        content.push_str("import RustBridgeC\n\n");

        // --- Delegate protocol (also in RustBridge so HtmlToMarkdown can conform to it) ---
        // The leading underscore is a Swift convention signalling "internal binding
        // surface, not part of the user-facing API". Marking it `internal` is not an
        // option: implementers (the private adapter class) live in a different module
        // and Swift access-control requires `public` for cross-module protocol conformance.
        content.push_str(&format!(
            "/// Delegate protocol for `{box_name}`.\n\
             /// Conforming types convert raw FFI params (RustString etc.) to user-friendly\n\
             /// Swift types and return a JSON-encoded result string.\n\
             /// Implemented by the private adapter class in the `{trait_name}` module.\n\
             ///\n\
             /// Leading `_` flags this as an internal binding surface — not part of the\n\
             /// user-facing API. Must remain `public` because Swift requires public visibility\n\
             /// for cross-module protocol conformance (implementer lives in the main target).\n\
             public protocol {delegate_protocol_name}: AnyObject {{\n"
        ));
        for method in &trait_def.methods {
            // Delegate protocol methods are part of the user-facing public Swift API
            // (consumers implement this protocol). Swift convention is lowerCamelCase
            // for method names, so we emit `processImage(...)` rather than the raw
            // Rust `process_image`. The shim's `alef_<snake>` C-ABI name remains
            // snake_case (matched by swift-bridge's @_cdecl exports) — only the
            // delegate-side method name is camelCased.
            let method_camel = swift_ident(&method.name.to_lower_camel_case());
            let delegate_params = swift_box_params(method);
            content.push_str(&format!("    func {method_camel}({delegate_params}) -> String\n"));
        }
        content.push_str("}\n\n");

        // --- Box class ---
        content.push_str(&format!(
            "/// Opaque box class retained by Rust via `Unmanaged<{box_name}>.passRetained`.\n\
             /// Each `alef_*` method corresponds to an `extern \"Swift\"` declaration in the\n\
             /// Rust bridge crate; swift-bridge generates @_cdecl shims that call these.\n\
             /// Delegates to a `{delegate_protocol_name}` (implemented in the main module).\n\
             public final class {box_name} {{\n\
             \x20   private let delegate: any {delegate_protocol_name}\n\
             \x20   public init(_ delegate: any {delegate_protocol_name}) {{ self.delegate = delegate }}\n"
        ));

        for method in &trait_def.methods {
            let method_snake = method.name.to_snake_case();
            // Shim name MUST stay snake_case — swift-bridge's @_cdecl exports on the
            // Rust side reference `__swift_bridge__$Swift{Trait}Box$alef_<method_snake>`.
            let shim_name = format!("alef_{method_snake}");
            // Box method params: keyword labels + RustString/RustVec types.
            let box_params = swift_box_params_keyword(method);
            // Args to pass to delegate (same names, no conversion needed — delegate takes same types).
            let delegate_call_args = swift_box_delegate_call_args(method);
            // Delegate call uses the camelCase protocol method (see protocol emission above).
            let method_camel = swift_ident(&method.name.to_lower_camel_case());
            content.push_str(&format!(
                "    public func {shim_name}({box_params}) -> String {{\n\
                 \x20       return delegate.{method_camel}({delegate_call_args})\n\
                 \x20   }}\n"
            ));
        }
        content.push_str("}\n");

        files.push(GeneratedFile {
            path: rust_bridge_dir.join(format!("Swift{trait_name}Box.swift")),
            content,
            generated_header: false,
        });
    }
    files
}

/// Map a TypeRef to the FFI type used by the `Swift{Trait}Box` class and its delegate protocol.
///
/// These types match exactly what swift-bridge `@_cdecl` shims pass into box methods:
/// - Rust `String` / `Named` → Swift `RustString`
/// - Rust `Option<String>` / `Option<Named>` → Swift `RustString?`
/// - Rust `Vec<String>` → Swift `RustVec<RustString>`
/// - Rust `usize` → Swift `UInt` (swift-bridge maps usize → UInt, not Int)
/// - Other primitives and booleans pass through unchanged.
fn swift_box_ffi_type(ty: &TypeRef, optional: bool) -> String {
    use crate::core::ir::PrimitiveType;
    let inner = match ty {
        TypeRef::String | TypeRef::Named(_) | TypeRef::Path | TypeRef::Json | TypeRef::Map(_, _) => {
            "RustString".to_string()
        }
        TypeRef::Optional(inner) => return format!("{}?", swift_box_ffi_type(inner, false)),
        TypeRef::Vec(inner) => format!("RustVec<{}>", swift_box_ffi_type(inner, false)),
        TypeRef::Primitive(PrimitiveType::Usize) | TypeRef::Primitive(PrimitiveType::Isize) => "UInt".to_string(),
        TypeRef::Primitive(PrimitiveType::Bool) => "Bool".to_string(),
        TypeRef::Primitive(PrimitiveType::U32) => "UInt32".to_string(),
        TypeRef::Primitive(PrimitiveType::U64) => "UInt64".to_string(),
        TypeRef::Primitive(PrimitiveType::I32) => "Int32".to_string(),
        TypeRef::Primitive(PrimitiveType::I64) => "Int64".to_string(),
        TypeRef::Primitive(PrimitiveType::F32) => "Float".to_string(),
        TypeRef::Primitive(PrimitiveType::F64) => "Double".to_string(),
        TypeRef::Primitive(PrimitiveType::U8) => "UInt8".to_string(),
        TypeRef::Primitive(PrimitiveType::I8) => "Int8".to_string(),
        TypeRef::Primitive(PrimitiveType::U16) => "UInt16".to_string(),
        TypeRef::Primitive(PrimitiveType::I16) => "Int16".to_string(),
        TypeRef::Bytes => "RustVec<UInt8>".to_string(),
        TypeRef::Char => "Character".to_string(),
        TypeRef::Duration => "Double".to_string(),
        TypeRef::Unit => "Void".to_string(),
    };
    if optional { format!("{inner}?") } else { inner }
}

/// Delegate protocol method params — positional (`_ name: FFIType`).
/// Types match `swift_box_ffi_type`: `RustString` for strings/named, `UInt` for usize.
/// Used in both the `Swift{Trait}BoxDelegate` protocol and the private adapter class
/// (which implements the delegate and converts to user-friendly types before delegation).
/// Param names are lowerCamelCase to match Swift API conventions.
fn swift_box_params(method: &crate::core::ir::MethodDef) -> String {
    let params: Vec<String> = method
        .params
        .iter()
        .map(|p| {
            let name = swift_ident(&p.name.to_lower_camel_case());
            let ty = swift_box_ffi_type(&p.ty, p.optional);
            format!("_ {name}: {ty}")
        })
        .collect();
    params.join(", ")
}

/// Box class method params with keyword argument labels (`external internal: FFIType`).
/// Used by the `public final class {Box}` methods exposed via `@_cdecl` shims.
///
/// The external label MUST be the raw snake_case Rust param name — that's what
/// swift-bridge 0.1.59 uses verbatim in the generated Swift call site (it takes the
/// Rust ident from the `extern "Swift"` block and emits `<rust_name>: <arg>`).
/// The internal name is lowerCamelCase, escaped via [`swift_ident`] for keyword
/// collisions, and is what the method body references when calling into the delegate.
///
/// When the external and internal names happen to match (single-word snake = camelCase
/// and not a keyword), we collapse them into a single token to keep the emitted Swift
/// idiomatic — `ctx: RustString` rather than `ctx ctx: RustString`.
fn swift_box_params_keyword(method: &crate::core::ir::MethodDef) -> String {
    let params: Vec<String> = method
        .params
        .iter()
        .map(|p| {
            let external = p.name.to_snake_case();
            let internal = swift_ident(&p.name.to_lower_camel_case());
            let ty = swift_box_ffi_type(&p.ty, p.optional);
            if external == internal {
                format!("{internal}: {ty}")
            } else {
                format!("{external} {internal}: {ty}")
            }
        })
        .collect();
    params.join(", ")
}

/// Generate setup lines + argument string for `inner.{methodCamel}(...)` inside the adapter.
///
/// The adapter receives box FFI types (`RustString`, `UInt`, `RustVec<RustString>`) and
/// must produce call args matching the user-facing protocol types (`String`, `Int`,
/// typed Swift structs for `Named` params, `RustVec<RustString>` for `Vec<String>`).
///
/// Conversions:
/// - `RustString` (Named, e.g. `NodeContext`) → decode via `try? {name}FromJson(...)`
///   into a typed Swift value. Emit a `let` setup line; pass the decoded value (or a
///   fallback default-constructed struct via JSON `"{}"` if decoding fails) as the arg.
///   Named decoding can fail at runtime if the Rust side ever emits malformed JSON;
///   we keep the protocol throws-free by falling back to `try! {name}FromJson("{}")`
///   which mirrors serde's default-on-empty contract for `#[serde(default)]` structs.
/// - `RustString` (plain String / Path / Json / Map) → `{name}.toString()`
/// - `RustString?` (Optional String etc.) → `{name}?.toString()`
/// - `UInt` (usize) → `Int({name})`
/// - `RustVec<...>`, `Bool`, `UInt32`, etc. → `{name}` (passed through)
///
/// Returns `(setup_lines, call_args)` — the setup_lines are inserted before the
/// `inner.{methodCamel}(...)` invocation (currently used for Named JSON decode).
fn swift_adapter_conversions(method: &crate::core::ir::MethodDef) -> (Vec<String>, String) {
    use crate::core::ir::PrimitiveType;
    let mut setup_lines: Vec<String> = Vec::new();
    let call_args: Vec<String> = method
        .params
        .iter()
        .map(|p| {
            // Param identifier matches the delegate-protocol label, which is lowerCamelCase
            // (see `swift_box_params` / `swift_box_params_keyword`). Local variable names
            // derived from it are also camelCase so they don't clash with the Swift keyword
            // escape produced by `swift_ident`.
            let snake = swift_ident(&p.name.to_lower_camel_case());
            match &p.ty {
                TypeRef::Named(name) => {
                    let from_json = format!("{name}FromJson").to_lower_camel_case();
                    let local = format!("{snake}Decoded");
                    if p.optional {
                        setup_lines.push(format!(
                            "let {local}: {name}? = {snake}.flatMap {{ try? {from_json}($0.toString()) }}"
                        ));
                    } else {
                        setup_lines.push(format!(
                            "let {local}: {name} = (try? {from_json}({snake}.toString())) ?? (try! {from_json}(\"{{}}\"))"
                        ));
                    }
                    local
                }
                TypeRef::String | TypeRef::Path | TypeRef::Json | TypeRef::Map(_, _) => {
                    if p.optional {
                        format!("{snake}?.toString()")
                    } else {
                        format!("{snake}.toString()")
                    }
                }
                TypeRef::Optional(inner) => match inner.as_ref() {
                    TypeRef::Named(name) => {
                        let from_json = format!("{name}FromJson").to_lower_camel_case();
                        let local = format!("{snake}Decoded");
                        setup_lines.push(format!(
                            "let {local}: {name}? = {snake}.flatMap {{ try? {from_json}($0.toString()) }}"
                        ));
                        local
                    }
                    TypeRef::String | TypeRef::Path | TypeRef::Json | TypeRef::Map(_, _) => {
                        format!("{snake}?.toString()")
                    }
                    _ => snake,
                },
                TypeRef::Primitive(PrimitiveType::Usize) | TypeRef::Primitive(PrimitiveType::Isize) => {
                    format!("Int({snake})")
                }
                _ => snake,
            }
        })
        .collect();
    (setup_lines, call_args.join(", "))
}

/// Argument list for forwarding from box class to delegate protocol method.
/// Produces `name1, name2` (positional, matching `_ name:` labels in protocol).
/// Names are lowerCamelCase to match the param labels emitted by
/// `swift_box_params` / `swift_box_params_keyword`.
fn swift_box_delegate_call_args(method: &crate::core::ir::MethodDef) -> String {
    method
        .params
        .iter()
        .map(|p| swift_ident(&p.name.to_lower_camel_case()))
        .collect::<Vec<_>>()
        .join(", ")
}

fn swift_protocol_params(method: &crate::core::ir::MethodDef) -> String {
    let params: Vec<String> = method
        .params
        .iter()
        .map(|p| {
            let name = p.name.to_lower_camel_case();
            let ty = swift_inbound_type(&p.ty, p.optional);
            format!("_ {name}: {ty}")
        })
        .collect();
    params.join(", ")
}

/// Protocol parameters with every argument prefixed by `_` (for the default extension impl).
fn swift_protocol_underscore_params(method: &crate::core::ir::MethodDef) -> String {
    let params: Vec<String> = method
        .params
        .iter()
        .map(|p| {
            let name = p.name.to_lower_camel_case();
            let ty = swift_inbound_type(&p.ty, p.optional);
            format!("_ _{name}: {ty}")
        })
        .collect();
    params.join(", ")
}

/// Argument list for the `inner.{methodCamel}(...)` delegation call.
/// `ctx` is `method.params[0]` so it is included naturally.
/// Param identifiers are lowerCamelCase to match the protocol-level labels.
#[allow(dead_code)]
fn swift_shim_call_args(method: &crate::core::ir::MethodDef) -> String {
    method
        .params
        .iter()
        .map(|p| swift_ident(&p.name.to_lower_camel_case()))
        .collect::<Vec<_>>()
        .join(", ")
}

/// Map a TypeRef to its Swift inbound bridge type string, mirroring `inbound_bridge_type`
/// from `plugin_inbound.rs`:
///   Named(name) → name (typed Swift struct/enum; adapter decodes JSON before calling)
///   Optional<Named> → name?
///   bool → Bool
///   u32 → UInt32
///   usize → Int
///   String → String
fn swift_inbound_type(ty: &TypeRef, optional: bool) -> String {
    use crate::core::ir::PrimitiveType;
    let inner = match ty {
        TypeRef::Named(name) => name.clone(),
        TypeRef::String => "String".to_string(),
        TypeRef::Primitive(PrimitiveType::Bool) => "Bool".to_string(),
        TypeRef::Primitive(PrimitiveType::U32) => "UInt32".to_string(),
        TypeRef::Primitive(PrimitiveType::U64) => "UInt64".to_string(),
        TypeRef::Primitive(PrimitiveType::I32) => "Int32".to_string(),
        TypeRef::Primitive(PrimitiveType::I64) => "Int64".to_string(),
        TypeRef::Primitive(PrimitiveType::Usize) => "Int".to_string(),
        TypeRef::Primitive(PrimitiveType::Isize) => "Int".to_string(),
        TypeRef::Primitive(PrimitiveType::F32) => "Float".to_string(),
        TypeRef::Primitive(PrimitiveType::F64) => "Double".to_string(),
        TypeRef::Primitive(PrimitiveType::U8) => "UInt8".to_string(),
        TypeRef::Primitive(PrimitiveType::I8) => "Int8".to_string(),
        TypeRef::Primitive(PrimitiveType::U16) => "UInt16".to_string(),
        TypeRef::Primitive(PrimitiveType::I16) => "Int16".to_string(),
        // Vec elements use the FFI type (RustString not String) because swift-bridge
        // sends RustVec<RustString> from the @_cdecl shim — the protocol must match.
        TypeRef::Vec(inner) => format!("RustVec<{}>", swift_box_ffi_type(inner, false)),
        TypeRef::Optional(inner) => return format!("{}?", swift_inbound_type(inner, false)),
        TypeRef::Unit => "Void".to_string(),
        TypeRef::Bytes => "RustVec<UInt8>".to_string(),
        TypeRef::Char => "Character".to_string(),
        TypeRef::Path => "String".to_string(),
        TypeRef::Json => "String".to_string(),
        TypeRef::Duration => "Double".to_string(),
        TypeRef::Map(_, _) => "String".to_string(),
    };
    if optional { format!("{inner}?") } else { inner }
}

/// Prepend `import RustBridgeC\n\n` to a swift-bridge-generated Swift file.
///
/// swift-bridge's `SwiftBridgeCore.swift` starts with `import Foundation` but does not
/// import the module that exposes the C types (`RustStr`, `__private__Option*`, etc.).
/// Under SwiftPM the `RustBridgeC` target is the designated carrier for those declarations,
/// and Swift source files in sibling targets must `import RustBridgeC` explicitly.
///
/// The crate-specific `{crate}-swift.swift` file has no imports at all and also
/// requires the same import.
///
/// Also prepends a `// swift-format-ignore-file` directive so Apple's
/// `swift-format` (typically wired into pre-commit on consumer repos) does not
/// rewrite the swift-bridge output on every regen. swift-bridge's emitter does
/// not honour swift-format's defaults (2-space indent, sorted imports, 100
/// char line width) and is intentionally not under our control — we copy its
/// output verbatim. Without the marker, every `alef all --clean` produces a
/// non-canonical file that the next `swift-format` hook rewrites, creating
/// noisy diffs and a stale-binding bisect trail.
///
/// Idempotent: if the file already starts with `import RustBridgeC` or the
/// `swift-format-ignore-file` directive is already present, neither is
/// duplicated.
/// Previously appended an `extension RustStringRef { func toString() }` shim
/// to swift-bridge's generated `SwiftBridgeCore.swift`. That approach broke
/// the Swift compiler with "non-'@objc' instance method 'toString()' is
/// declared in extension of 'RustStringRef' and cannot be overridden",
/// because `RustString` is a subclass of `RustStringRef` and declares its
/// own `toString()` in another extension — Swift cannot resolve the override
/// chain across two non-@objc extensions in the same hierarchy.
///
/// We now emit `$0.as_str().toString()` directly at every callsite and
/// preserve this no-op function as a marker that the codegen contract has
/// shifted (it also strips any stale shim previously appended by an older
/// alef revision).
fn append_rust_string_ref_to_string_extension(content: &str) -> String {
    const MARKER: &str = "// alef: RustStringRef.toString() shim";
    if let Some(idx) = content.find(MARKER) {
        // Strip the previously-appended shim (everything from MARKER to EOF).
        let mut head = content[..idx].to_string();
        while head.ends_with('\n') {
            head.pop();
        }
        head.push('\n');
        head
    } else {
        content.to_string()
    }
}

fn prepend_rust_bridge_c_import(content: &str) -> String {
    const IMPORT: &str = "import RustBridgeC";
    const IGNORE: &str = "// swift-format-ignore-file";

    let head: Vec<&str> = content.lines().take(5).collect();
    let has_import = head.iter().any(|l| l.trim() == IMPORT);
    let has_ignore = head.iter().any(|l| l.trim() == IGNORE);

    match (has_import, has_ignore) {
        (true, true) => content.to_string(),
        (true, false) => format!("{IGNORE}\n{content}"),
        (false, true) => format!("{IMPORT}\n\n{content}"),
        (false, false) => format!("{IGNORE}\n{IMPORT}\n\n{content}"),
    }
}

/// Returns `true` when `ty` is an untagged serde enum (or `Optional` wrapping one).
///
/// Untagged enums (`#[serde(untagged)]`) are bridged as a JSON-encoded `RustString` at
/// the FFI boundary because swift-bridge cannot represent their variant structure natively.
/// This means:
/// - The swift-bridge getter returns `RustString` (not `RustBridge.{Name}Ref`).
/// - There is no `init(_ rb: RustBridge.{Name}Ref)` initializer for untagged enums.
/// - Field init code must decode via `JSONDecoder` rather than the Ref-based path.
///
/// Covers the plain `TypeRef::Named(n)` and `TypeRef::Optional(TypeRef::Named(n))` cases
/// that are checked at the field-init call site. Vec<UntaggedEnum> is handled separately
/// inside `swift_ffi_read_expr`.
fn is_untagged_enum_type(
    ty: &crate::core::ir::TypeRef,
    untagged_enum_names: &std::collections::HashSet<String>,
) -> bool {
    use crate::core::ir::TypeRef;
    match ty {
        TypeRef::Named(n) => untagged_enum_names.contains(n),
        TypeRef::Optional(inner) => is_untagged_enum_type(inner, untagged_enum_names),
        _ => false,
    }
}

/// Mirror of `gen_rust_crate::type_bridge::needs_json_bridge` (kept private here to
/// avoid bringing the Rust-crate emission module into the Swift-binding module's
/// dependency graph). A field type that needs the JSON bridge on the Rust side
/// will surface as a `RustString` accessor return on the Swift side — the init
/// path must decode the JSON string back into the typed Swift property.
fn needs_json_bridge_for_swift(ty: &crate::core::ir::TypeRef) -> bool {
    use crate::core::ir::TypeRef;
    fn is_leaf(ty: &TypeRef) -> bool {
        matches!(
            ty,
            TypeRef::Primitive(_)
                | TypeRef::String
                | TypeRef::Char
                | TypeRef::Path
                | TypeRef::Json
                | TypeRef::Unit
                | TypeRef::Duration
                | TypeRef::Bytes
                | TypeRef::Named(_),
        )
    }
    match ty {
        TypeRef::Map(_, _) => true,
        // Vec is JSON-bridged only when the inner type is not a "leaf" (Vec<Vec<…>>,
        // Vec<Option<…>>, Vec<Map<…>>). Plain Vec<T> for leaf T crosses as RustVec<T>.
        TypeRef::Vec(inner) => !is_leaf(inner),
        // Plain Optional<T> for primitives/strings crosses natively (`Option<T>`),
        // so it is NOT JSON-bridged. Only Optional wrapping a JSON-bridged inner
        // (Optional<Vec<Vec<…>>>, Optional<Map<…>>) needs JSON decoding.
        TypeRef::Optional(inner) => needs_json_bridge_for_swift(inner),
        _ => false,
    }
}

/// Returns `true` when the wrapper-side getter for `field` is skipped by the
/// Rust crate (via `is_unbridgeable_getter`) — in which case the Swift `init(rb:)`
/// must not call `rb.{field}()` because the accessor does not exist.
fn is_field_unbridgeable_for_init(
    ty: &crate::core::ir::TypeDef,
    field: &crate::core::ir::FieldDef,
    exclude_fields: &std::collections::HashSet<String>,
    known_dto_names: &std::collections::HashSet<String>,
) -> bool {
    use crate::core::ir::TypeRef;
    use heck::ToSnakeCase;
    let name = field.name.to_snake_case();
    let field_key = format!("{}.{}", ty.name, name);
    if field.binding_excluded || exclude_fields.contains(&field_key) {
        return true;
    }
    // Sanitized Vec field whose inner is not a primitive/bytes — wrapper skips emit.
    if let TypeRef::Vec(inner) = &field.ty
        && field.sanitized
        && !matches!(inner.as_ref(), TypeRef::Primitive(_) | TypeRef::Bytes)
    {
        return true;
    }
    // Vec<Named> on non-serde struct — wrapper skips emit.
    if !ty.has_serde
        && let TypeRef::Vec(inner) = &field.ty
        && !matches!(inner.as_ref(), TypeRef::Primitive(_) | TypeRef::Bytes)
    {
        return true;
    }
    // JSON-bridge with inner Named where the wrapper does not exist.
    if needs_json_bridge_for_swift(&field.ty) {
        let inner_named = match &field.ty {
            TypeRef::Optional(inner) | TypeRef::Vec(inner) => match inner.as_ref() {
                TypeRef::Named(n) => Some(n.as_str()),
                _ => None,
            },
            TypeRef::Named(n) => Some(n.as_str()),
            _ => None,
        };
        if let Some(n) = inner_named
            && !known_dto_names.contains(n)
        {
            return true;
        }
    }
    false
}

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

    /// Fresh swift-bridge output (no import, no ignore directive) must receive
    /// both prepends so the file is canonical for `swift-format` from the first
    /// regen onwards.
    #[test]
    fn prepend_adds_ignore_and_import_for_fresh_content() {
        let bridge_out = "import Foundation\n\npublic class Foo {}\n";
        let result = prepend_rust_bridge_c_import(bridge_out);
        assert!(
            result.starts_with("// swift-format-ignore-file\nimport RustBridgeC\n\n"),
            "expected ignore directive then RustBridgeC import as the file prologue, got: {result:?}",
        );
        assert!(result.contains("public class Foo {}"));
    }

    /// Re-running alef on a previously prepended file must be a no-op so the
    /// embedded hash stays stable across regens.
    #[test]
    fn prepend_is_idempotent_when_both_present() {
        let already_prepared =
            "// swift-format-ignore-file\nimport RustBridgeC\n\nimport Foundation\n\npublic class Foo {}\n";
        let result = prepend_rust_bridge_c_import(already_prepared);
        assert_eq!(result, already_prepared, "second pass must not duplicate directives");
    }

    /// Older on-disk content that already carries the import (pre-fix) must
    /// have only the missing ignore directive added — without re-importing.
    #[test]
    fn prepend_adds_only_ignore_when_import_present() {
        let import_only = "import RustBridgeC\n\nimport Foundation\n\npublic class Foo {}\n";
        let result = prepend_rust_bridge_c_import(import_only);
        assert_eq!(
            result, "// swift-format-ignore-file\nimport RustBridgeC\n\nimport Foundation\n\npublic class Foo {}\n",
            "missing ignore directive must be prepended without duplicating the import line",
        );
        assert_eq!(
            result.matches("import RustBridgeC").count(),
            1,
            "import RustBridgeC must appear exactly once",
        );
    }

    /// Content with only the ignore directive (hypothetical) must have the
    /// import added without duplicating the directive.
    #[test]
    fn prepend_adds_only_import_when_ignore_present() {
        let ignore_only = "// swift-format-ignore-file\nimport Foundation\n\npublic class Foo {}\n";
        let result = prepend_rust_bridge_c_import(ignore_only);
        assert!(
            result.starts_with("import RustBridgeC\n\n// swift-format-ignore-file\n"),
            "expected import prepended ahead of pre-existing ignore directive, got: {result:?}",
        );
        assert_eq!(
            result.matches("// swift-format-ignore-file").count(),
            1,
            "ignore directive must appear exactly once",
        );
    }

    /// Fresh swift-bridge output must pass through unchanged — we no longer
    /// append a `RustStringRef.toString()` shim because Swift rejects it as
    /// a non-@objc override of `RustString.toString()` (RustString is a
    /// subclass of RustStringRef).
    #[test]
    fn append_passes_through_when_no_marker() {
        let core_swift = "import Foundation\n\npublic class RustString {}\n";
        let result = append_rust_string_ref_to_string_extension(core_swift);
        assert_eq!(result, core_swift, "no-op when the marker is absent");
    }

    /// A previously-appended shim must be stripped on regen so old workspaces
    /// don't keep the broken extension after upgrading alef.
    #[test]
    fn append_strips_legacy_shim_when_present() {
        let core_swift = "import Foundation\n\npublic class RustString {}\n\n\
            // alef: RustStringRef.toString() shim\n\
            extension RustStringRef {\n    public func toString() -> String { self.as_str().toString() }\n}\n";
        let result = append_rust_string_ref_to_string_extension(core_swift);
        assert!(
            !result.contains("// alef: RustStringRef.toString() shim"),
            "expected the legacy shim to be stripped, got: {result:?}",
        );
        assert!(
            result.contains("public class RustString"),
            "non-shim content must be preserved verbatim",
        );
    }

    /// The forwarder must convert `Optional<RustBridge.{Dto}>` returned by a
    /// swift-bridge call into the high-level `Optional<{Dto}>` struct via the
    /// generated `init(_ rb:) throws` initializer. The conversion uses
    /// `Optional.map { try {Dto}($0) }`, which rethrows through the enclosing
    /// forwarder.
    #[test]
    fn optional_named_dto_return_emits_throwing_map_conversion() {
        let mut known: std::collections::HashSet<String> = Default::default();
        known.insert("EmbeddingPreset".to_string());
        let ty = TypeRef::Optional(Box::new(TypeRef::Named("EmbeddingPreset".to_string())));
        let suffix = forwarder_return_conversion_suffix_with_throws(&ty, &known, false);
        assert_eq!(suffix, ".map { try EmbeddingPreset($0) }");
        assert!(
            return_value_conversion_throws(&ty, &known),
            "Optional<Named DTO> conversion must report as throwing so the outer forwarder is `throws`",
        );
    }

    /// A non-DTO Named return (e.g. an opaque handle type that does not have
    /// a high-level struct counterpart) must NOT have its return value
    /// rewritten — keeping the existing pass-through behaviour intact.
    #[test]
    fn optional_non_dto_named_return_passes_through_unchanged() {
        let known: std::collections::HashSet<String> = Default::default();
        let ty = TypeRef::Optional(Box::new(TypeRef::Named("OpaqueHandle".to_string())));
        let suffix = forwarder_return_conversion_suffix_with_throws(&ty, &known, false);
        assert!(suffix.is_empty(), "non-DTO Named return must not emit any suffix");
        assert!(!return_value_conversion_throws(&ty, &known));
    }
}
