Skip to main content

alef_backend_java/
gen_bindings.rs

1use crate::type_map::{java_boxed_type, java_ffi_type, java_type};
2use ahash::AHashSet;
3use alef_codegen::naming::{to_class_name, to_java_name};
4use alef_core::backend::{Backend, BuildConfig, Capabilities, GeneratedFile};
5use alef_core::config::{AlefConfig, Language, resolve_output_dir};
6use alef_core::ir::{ApiSurface, EnumDef, FunctionDef, PrimitiveType, TypeDef, TypeRef};
7use heck::{ToLowerCamelCase, ToPascalCase, ToSnakeCase};
8use std::collections::HashSet;
9use std::fmt::Write;
10use std::path::PathBuf;
11
12/// Names that conflict with methods on `java.lang.Object` and are therefore
13/// illegal as record component names or method names in generated Java code.
14const JAVA_OBJECT_METHOD_NAMES: &[&str] = &[
15    "wait",
16    "notify",
17    "notifyAll",
18    "getClass",
19    "hashCode",
20    "equals",
21    "toString",
22    "clone",
23    "finalize",
24];
25
26/// Returns true if `name` is a tuple/unnamed field index such as `"0"`, `"1"`, `"_0"`, `"_1"`.
27/// Serde represents tuple and newtype variant fields with these numeric names. They are not
28/// real JSON keys and must not be used as Java identifiers.
29/// Escape a string for use inside a Javadoc comment.
30/// Replaces `*/` (which would close the comment) and `@` (which starts a tag).
31fn escape_javadoc_line(s: &str) -> String {
32    let mut result = String::with_capacity(s.len());
33    let mut chars = s.chars().peekable();
34    while let Some(ch) = chars.next() {
35        if ch == '`' {
36            let mut code = String::new();
37            for c in chars.by_ref() {
38                if c == '`' {
39                    break;
40                }
41                code.push(c);
42            }
43            result.push_str("{@code ");
44            result.push_str(&code);
45            result.push('}');
46        } else if ch == '<' {
47            result.push_str("&lt;");
48        } else if ch == '>' {
49            result.push_str("&gt;");
50        } else if ch == '&' {
51            result.push_str("&amp;");
52        } else if ch == '*' && chars.peek() == Some(&'/') {
53            chars.next();
54            result.push_str("* /");
55        } else if ch == '@' {
56            result.push_str("{@literal @}");
57        } else {
58            result.push(ch);
59        }
60    }
61    result
62}
63
64fn is_tuple_field_name(name: &str) -> bool {
65    let stripped = name.trim_start_matches('_');
66    !stripped.is_empty() && stripped.chars().all(|c| c.is_ascii_digit())
67}
68
69/// Sanitise a field/parameter name that would conflict with `java.lang.Object`
70/// methods.  Conflicting names get a `_` suffix (e.g. `wait` -> `wait_`), which
71/// is then converted to camelCase by `to_java_name`.
72fn safe_java_field_name(name: &str) -> String {
73    let java_name = to_java_name(name);
74    if JAVA_OBJECT_METHOD_NAMES.contains(&java_name.as_str()) {
75        format!("{}Value", java_name)
76    } else {
77        java_name
78    }
79}
80
81pub struct JavaBackend;
82
83impl JavaBackend {
84    /// Convert crate name to main class name (PascalCase + "Rs" suffix).
85    ///
86    /// The "Rs" suffix ensures the raw FFI wrapper class has a distinct name from
87    /// the public facade class (which strips the "Rs" suffix). Without this, the
88    /// facade would delegate to itself, causing infinite recursion.
89    fn resolve_main_class(api: &ApiSurface) -> String {
90        let base = to_class_name(&api.crate_name.replace('-', "_"));
91        if base.ends_with("Rs") {
92            base
93        } else {
94            format!("{}Rs", base)
95        }
96    }
97}
98
99impl Backend for JavaBackend {
100    fn name(&self) -> &str {
101        "java"
102    }
103
104    fn language(&self) -> Language {
105        Language::Java
106    }
107
108    fn capabilities(&self) -> Capabilities {
109        Capabilities {
110            supports_async: true,
111            supports_classes: true,
112            supports_enums: true,
113            supports_option: true,
114            supports_result: true,
115            ..Capabilities::default()
116        }
117    }
118
119    fn generate_bindings(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
120        let package = config.java_package();
121        let prefix = config.ffi_prefix();
122        let main_class = Self::resolve_main_class(api);
123        let package_path = package.replace('.', "/");
124
125        let output_dir = resolve_output_dir(
126            config.output.java.as_ref(),
127            &config.crate_config.name,
128            "packages/java/src/main/java/",
129        );
130
131        let base_path = PathBuf::from(&output_dir).join(&package_path);
132
133        // Collect bridge param names and type aliases so we can strip them from generated
134        // function signatures and emit convertWithVisitor instead.
135        let bridge_param_names: HashSet<String> = config
136            .trait_bridges
137            .iter()
138            .filter_map(|b| b.param_name.clone())
139            .collect();
140        let bridge_type_aliases: HashSet<String> = config
141            .trait_bridges
142            .iter()
143            .filter_map(|b| b.type_alias.clone())
144            .collect();
145        let has_visitor_bridge = !config.trait_bridges.is_empty();
146
147        let mut files = Vec::new();
148
149        // 0. package-info.java - required by Checkstyle
150        let description = config
151            .scaffold
152            .as_ref()
153            .and_then(|s| s.description.as_deref())
154            .unwrap_or("High-performance HTML to Markdown converter.");
155        files.push(GeneratedFile {
156            path: base_path.join("package-info.java"),
157            content: format!(
158                "/**\n * {description}\n */\npackage {package};\n",
159                description = description,
160                package = package,
161            ),
162            generated_header: true,
163        });
164
165        // 1. NativeLib.java - FFI method handles
166        files.push(GeneratedFile {
167            path: base_path.join("NativeLib.java"),
168            content: gen_native_lib(api, config, &package, &prefix, has_visitor_bridge),
169            generated_header: true,
170        });
171
172        // 2. Main wrapper class
173        files.push(GeneratedFile {
174            path: base_path.join(format!("{}.java", main_class)),
175            content: gen_main_class(
176                api,
177                config,
178                &package,
179                &main_class,
180                &prefix,
181                &bridge_param_names,
182                &bridge_type_aliases,
183                has_visitor_bridge,
184            ),
185            generated_header: true,
186        });
187
188        // 3. Exception class
189        files.push(GeneratedFile {
190            path: base_path.join(format!("{}Exception.java", main_class)),
191            content: gen_exception_class(&package, &main_class),
192            generated_header: true,
193        });
194
195        // Collect complex enums (enums with data variants and no serde tag) — use Object for these fields.
196        // Tagged unions (serde_tag is set) are now generated as proper sealed interfaces
197        // and can be deserialized as their concrete types, so they are NOT complex_enums.
198        let complex_enums: AHashSet<String> = api
199            .enums
200            .iter()
201            .filter(|e| e.serde_tag.is_none() && e.variants.iter().any(|v| !v.fields.is_empty()))
202            .map(|e| e.name.clone())
203            .collect();
204
205        // Resolve language-level serde rename strategy (always wins over IR type-level).
206        let lang_rename_all = config.serde_rename_all_for_language(Language::Java);
207
208        // 4. Record types
209        for typ in api.types.iter().filter(|typ| !typ.is_trait) {
210            if !typ.is_opaque && !typ.fields.is_empty() {
211                files.push(GeneratedFile {
212                    path: base_path.join(format!("{}.java", typ.name)),
213                    content: gen_record_type(&package, typ, &complex_enums, &lang_rename_all),
214                    generated_header: true,
215                });
216                // Generate builder class for types with defaults
217                if typ.has_default {
218                    files.push(GeneratedFile {
219                        path: base_path.join(format!("{}Builder.java", typ.name)),
220                        content: gen_builder_class(&package, typ),
221                        generated_header: true,
222                    });
223                }
224            }
225        }
226
227        // Collect builder class names generated from record types with defaults,
228        // so we can skip opaque types that would collide with them.
229        let builder_class_names: AHashSet<String> = api
230            .types
231            .iter()
232            .filter(|t| !t.is_opaque && !t.fields.is_empty() && t.has_default)
233            .map(|t| format!("{}Builder", t.name))
234            .collect();
235
236        // 4b. Opaque handle types (skip if a pure-Java builder already covers this name)
237        for typ in api.types.iter().filter(|typ| !typ.is_trait) {
238            if typ.is_opaque && !builder_class_names.contains(&typ.name) {
239                files.push(GeneratedFile {
240                    path: base_path.join(format!("{}.java", typ.name)),
241                    content: gen_opaque_handle_class(&package, typ, &prefix),
242                    generated_header: true,
243                });
244            }
245        }
246
247        // 5. Enums
248        for enum_def in &api.enums {
249            files.push(GeneratedFile {
250                path: base_path.join(format!("{}.java", enum_def.name)),
251                content: gen_enum_class(&package, enum_def),
252                generated_header: true,
253            });
254        }
255
256        // 6. Error exception classes
257        for error in &api.errors {
258            for (class_name, content) in alef_codegen::error_gen::gen_java_error_types(error, &package) {
259                files.push(GeneratedFile {
260                    path: base_path.join(format!("{}.java", class_name)),
261                    content,
262                    generated_header: true,
263                });
264            }
265        }
266
267        // 7. Visitor support files (when a trait bridge is configured)
268        if has_visitor_bridge {
269            for (filename, content) in crate::gen_visitor::gen_visitor_files(&package, &main_class) {
270                files.push(GeneratedFile {
271                    path: base_path.join(filename),
272                    content,
273                    generated_header: false, // already has header comment
274                });
275            }
276        }
277
278        // Build adapter body map (consumed by generators via body substitution)
279        let _adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Java)?;
280
281        Ok(files)
282    }
283
284    fn generate_public_api(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
285        let package = config.java_package();
286        let prefix = config.ffi_prefix();
287        let main_class = Self::resolve_main_class(api);
288        let package_path = package.replace('.', "/");
289
290        let output_dir = resolve_output_dir(
291            config.output.java.as_ref(),
292            &config.crate_config.name,
293            "packages/java/src/main/java/",
294        );
295
296        let base_path = PathBuf::from(&output_dir).join(&package_path);
297
298        // Collect bridge param names/aliases to strip from the public facade.
299        let bridge_param_names: HashSet<String> = config
300            .trait_bridges
301            .iter()
302            .filter_map(|b| b.param_name.clone())
303            .collect();
304        let bridge_type_aliases: HashSet<String> = config
305            .trait_bridges
306            .iter()
307            .filter_map(|b| b.type_alias.clone())
308            .collect();
309        let has_visitor_bridge = !config.trait_bridges.is_empty();
310
311        // Generate a high-level public API class that wraps the raw FFI class.
312        // Class name = main_class without "Rs" suffix (e.g., HtmlToMarkdownRs -> HtmlToMarkdown)
313        let public_class = main_class.trim_end_matches("Rs").to_string();
314        let facade_content = gen_facade_class(
315            api,
316            &package,
317            &public_class,
318            &main_class,
319            &prefix,
320            &bridge_param_names,
321            &bridge_type_aliases,
322            has_visitor_bridge,
323        );
324
325        Ok(vec![GeneratedFile {
326            path: base_path.join(format!("{}.java", public_class)),
327            content: facade_content,
328            generated_header: true,
329        }])
330    }
331
332    fn build_config(&self) -> Option<BuildConfig> {
333        Some(BuildConfig {
334            tool: "mvn",
335            crate_suffix: "",
336            depends_on_ffi: true,
337            post_build: vec![],
338        })
339    }
340}
341
342// ---------------------------------------------------------------------------
343// NativeLib.java - FFI method handles
344// ---------------------------------------------------------------------------
345
346fn gen_native_lib(
347    api: &ApiSurface,
348    config: &AlefConfig,
349    package: &str,
350    prefix: &str,
351    has_visitor_bridge: bool,
352) -> String {
353    // Generate the class body first, then scan it to determine which imports are needed.
354    let mut body = String::with_capacity(2048);
355    // Derive the native library name from the FFI output path (directory name with hyphens replaced
356    // by underscores), falling back to `{ffi_prefix}_ffi`.
357    let lib_name = config.ffi_lib_name();
358
359    writeln!(body, "final class NativeLib {{").ok();
360    writeln!(body, "    private static final Linker LINKER = Linker.nativeLinker();").ok();
361    writeln!(body, "    private static final SymbolLookup LIB;").ok();
362    writeln!(
363        body,
364        "    private static final String NATIVES_RESOURCE_ROOT = \"/natives\";"
365    )
366    .ok();
367    writeln!(
368        body,
369        "    private static final Object NATIVE_EXTRACT_LOCK = new Object();"
370    )
371    .ok();
372    writeln!(body, "    private static String cachedExtractKey;").ok();
373    writeln!(body, "    private static Path cachedExtractDir;").ok();
374    writeln!(body).ok();
375    writeln!(body, "    static {{").ok();
376    writeln!(body, "        loadNativeLibrary();").ok();
377    writeln!(body, "        LIB = SymbolLookup.loaderLookup();").ok();
378    writeln!(body, "    }}").ok();
379    writeln!(body).ok();
380    writeln!(body, "    private static void loadNativeLibrary() {{").ok();
381    writeln!(
382        body,
383        "        String osName = System.getProperty(\"os.name\", \"\").toLowerCase(java.util.Locale.ROOT);"
384    )
385    .ok();
386    writeln!(
387        body,
388        "        String osArch = System.getProperty(\"os.arch\", \"\").toLowerCase(java.util.Locale.ROOT);"
389    )
390    .ok();
391    writeln!(body).ok();
392    writeln!(body, "        String libName;").ok();
393    writeln!(body, "        String libExt;").ok();
394    writeln!(
395        body,
396        "        if (osName.contains(\"mac\") || osName.contains(\"darwin\")) {{"
397    )
398    .ok();
399    writeln!(body, "            libName = \"lib{}\";", lib_name).ok();
400    writeln!(body, "            libExt = \".dylib\";").ok();
401    writeln!(body, "        }} else if (osName.contains(\"win\")) {{").ok();
402    writeln!(body, "            libName = \"{}\";", lib_name).ok();
403    writeln!(body, "            libExt = \".dll\";").ok();
404    writeln!(body, "        }} else {{").ok();
405    writeln!(body, "            libName = \"lib{}\";", lib_name).ok();
406    writeln!(body, "            libExt = \".so\";").ok();
407    writeln!(body, "        }}").ok();
408    writeln!(body).ok();
409    writeln!(body, "        String nativesRid = resolveNativesRid(osName, osArch);").ok();
410    writeln!(
411        body,
412        "        String nativesDir = NATIVES_RESOURCE_ROOT + \"/\" + nativesRid;"
413    )
414    .ok();
415    writeln!(body).ok();
416    writeln!(
417        body,
418        "        Path extracted = tryExtractAndLoadFromResources(nativesDir, libName, libExt);"
419    )
420    .ok();
421    writeln!(body, "        if (extracted != null) {{").ok();
422    writeln!(body, "            return;").ok();
423    writeln!(body, "        }}").ok();
424    writeln!(body).ok();
425    writeln!(body, "        try {{").ok();
426    writeln!(body, "            System.loadLibrary(\"{}\");", lib_name).ok();
427    writeln!(body, "        }} catch (UnsatisfiedLinkError e) {{").ok();
428    writeln!(
429        body,
430        "            String msg = \"Failed to load {} native library. Expected resource: \" + nativesDir + \"/\" + libName",
431        lib_name
432    ).ok();
433    writeln!(
434        body,
435        "                    + libExt + \" (RID: \" + nativesRid + \"). \""
436    )
437    .ok();
438    writeln!(
439        body,
440        "                    + \"Ensure the library is bundled in the JAR under natives/{{os-arch}}/, \""
441    )
442    .ok();
443    writeln!(
444        body,
445        "                    + \"or place it on the system library path (java.library.path).\";",
446    )
447    .ok();
448    writeln!(
449        body,
450        "            UnsatisfiedLinkError out = new UnsatisfiedLinkError(msg + \" Original error: \" + e.getMessage());"
451    )
452    .ok();
453    writeln!(body, "            out.initCause(e);").ok();
454    writeln!(body, "            throw out;").ok();
455    writeln!(body, "        }}").ok();
456    writeln!(body, "    }}").ok();
457    writeln!(body).ok();
458    writeln!(
459        body,
460        "    private static Path tryExtractAndLoadFromResources(String nativesDir, String libName, String libExt) {{"
461    )
462    .ok();
463    writeln!(
464        body,
465        "        String resourcePath = nativesDir + \"/\" + libName + libExt;"
466    )
467    .ok();
468    writeln!(
469        body,
470        "        URL resource = NativeLib.class.getResource(resourcePath);"
471    )
472    .ok();
473    writeln!(body, "        if (resource == null) {{").ok();
474    writeln!(body, "            return null;").ok();
475    writeln!(body, "        }}").ok();
476    writeln!(body).ok();
477    writeln!(body, "        try {{").ok();
478    writeln!(
479        body,
480        "            Path tempDir = extractOrReuseNativeDirectory(nativesDir);"
481    )
482    .ok();
483    writeln!(body, "            Path libPath = tempDir.resolve(libName + libExt);").ok();
484    writeln!(body, "            if (!Files.exists(libPath)) {{").ok();
485    writeln!(
486        body,
487        "                throw new UnsatisfiedLinkError(\"Missing extracted native library: \" + libPath);"
488    )
489    .ok();
490    writeln!(body, "            }}").ok();
491    writeln!(body, "            System.load(libPath.toAbsolutePath().toString());").ok();
492    writeln!(body, "            return libPath;").ok();
493    writeln!(body, "        }} catch (Exception e) {{").ok();
494    writeln!(body, "            System.err.println(\"[NativeLib] Failed to extract and load native library from resources: \" + e.getMessage());").ok();
495    writeln!(body, "            return null;").ok();
496    writeln!(body, "        }}").ok();
497    writeln!(body, "    }}").ok();
498    writeln!(body).ok();
499    writeln!(
500        body,
501        "    private static Path extractOrReuseNativeDirectory(String nativesDir) throws Exception {{"
502    )
503    .ok();
504    writeln!(
505        body,
506        "        URL location = NativeLib.class.getProtectionDomain().getCodeSource().getLocation();"
507    )
508    .ok();
509    writeln!(body, "        if (location == null) {{").ok();
510    writeln!(
511        body,
512        "            throw new IllegalStateException(\"Missing code source location for {} JAR\");",
513        lib_name
514    )
515    .ok();
516    writeln!(body, "        }}").ok();
517    writeln!(body).ok();
518    writeln!(body, "        Path codePath = Path.of(location.toURI());").ok();
519    writeln!(
520        body,
521        "        String key = codePath.toAbsolutePath() + \"::\" + nativesDir;"
522    )
523    .ok();
524    writeln!(body).ok();
525    writeln!(body, "        synchronized (NATIVE_EXTRACT_LOCK) {{").ok();
526    writeln!(
527        body,
528        "            if (cachedExtractDir != null && key.equals(cachedExtractKey)) {{"
529    )
530    .ok();
531    writeln!(body, "                return cachedExtractDir;").ok();
532    writeln!(body, "            }}").ok();
533    writeln!(
534        body,
535        "            Path tempDir = Files.createTempDirectory(\"{}_native\");",
536        lib_name
537    )
538    .ok();
539    writeln!(body, "            tempDir.toFile().deleteOnExit();").ok();
540    writeln!(
541        body,
542        "            List<Path> extracted = extractNativeDirectory(codePath, nativesDir, tempDir);"
543    )
544    .ok();
545    writeln!(body, "            if (extracted.isEmpty()) {{").ok();
546    writeln!(body, "                throw new IllegalStateException(\"No native files extracted from resources dir: \" + nativesDir);").ok();
547    writeln!(body, "            }}").ok();
548    writeln!(body, "            cachedExtractKey = key;").ok();
549    writeln!(body, "            cachedExtractDir = tempDir;").ok();
550    writeln!(body, "            return tempDir;").ok();
551    writeln!(body, "        }}").ok();
552    writeln!(body, "    }}").ok();
553    writeln!(body).ok();
554    writeln!(body, "    private static List<Path> extractNativeDirectory(Path codePath, String nativesDir, Path destDir) throws Exception {{").ok();
555    writeln!(
556        body,
557        "        if (!Files.exists(destDir) || !Files.isDirectory(destDir)) {{"
558    )
559    .ok();
560    writeln!(
561        body,
562        "            throw new IllegalArgumentException(\"Destination directory does not exist: \" + destDir);"
563    )
564    .ok();
565    writeln!(body, "        }}").ok();
566    writeln!(body).ok();
567    writeln!(
568        body,
569        "        String prefix = nativesDir.startsWith(\"/\") ? nativesDir.substring(1) : nativesDir;"
570    )
571    .ok();
572    writeln!(body, "        if (!prefix.endsWith(\"/\")) {{").ok();
573    writeln!(body, "            prefix = prefix + \"/\";").ok();
574    writeln!(body, "        }}").ok();
575    writeln!(body).ok();
576    writeln!(body, "        if (Files.isDirectory(codePath)) {{").ok();
577    writeln!(body, "            Path nativesPath = codePath.resolve(prefix);").ok();
578    writeln!(
579        body,
580        "            if (!Files.exists(nativesPath) || !Files.isDirectory(nativesPath)) {{"
581    )
582    .ok();
583    writeln!(body, "                return List.of();").ok();
584    writeln!(body, "            }}").ok();
585    writeln!(body, "            return copyDirectory(nativesPath, destDir);").ok();
586    writeln!(body, "        }}").ok();
587    writeln!(body).ok();
588    writeln!(body, "        List<Path> extracted = new ArrayList<>();").ok();
589    writeln!(body, "        try (JarFile jar = new JarFile(codePath.toFile())) {{").ok();
590    writeln!(body, "            Enumeration<JarEntry> entries = jar.entries();").ok();
591    writeln!(body, "            while (entries.hasMoreElements()) {{").ok();
592    writeln!(body, "                JarEntry entry = entries.nextElement();").ok();
593    writeln!(body, "                String name = entry.getName();").ok();
594    writeln!(
595        body,
596        "                if (!name.startsWith(prefix) || entry.isDirectory()) {{"
597    )
598    .ok();
599    writeln!(body, "                    continue;").ok();
600    writeln!(body, "                }}").ok();
601    writeln!(
602        body,
603        "                String relative = name.substring(prefix.length());"
604    )
605    .ok();
606    writeln!(body, "                Path out = safeResolve(destDir, relative);").ok();
607    writeln!(body, "                Files.createDirectories(out.getParent());").ok();
608    writeln!(body, "                try (var in = jar.getInputStream(entry)) {{").ok();
609    writeln!(
610        body,
611        "                    Files.copy(in, out, StandardCopyOption.REPLACE_EXISTING);"
612    )
613    .ok();
614    writeln!(body, "                }}").ok();
615    writeln!(body, "                out.toFile().deleteOnExit();").ok();
616    writeln!(body, "                extracted.add(out);").ok();
617    writeln!(body, "            }}").ok();
618    writeln!(body, "        }}").ok();
619    writeln!(body, "        return extracted;").ok();
620    writeln!(body, "    }}").ok();
621    writeln!(body).ok();
622    writeln!(
623        body,
624        "    private static List<Path> copyDirectory(Path srcDir, Path destDir) throws Exception {{"
625    )
626    .ok();
627    writeln!(body, "        List<Path> copied = new ArrayList<>();").ok();
628    writeln!(body, "        try (var paths = Files.walk(srcDir)) {{").ok();
629    writeln!(body, "            for (Path src : (Iterable<Path>) paths::iterator) {{").ok();
630    writeln!(body, "                if (Files.isDirectory(src)) {{").ok();
631    writeln!(body, "                    continue;").ok();
632    writeln!(body, "                }}").ok();
633    writeln!(body, "                Path relative = srcDir.relativize(src);").ok();
634    writeln!(
635        body,
636        "                Path out = safeResolve(destDir, relative.toString());"
637    )
638    .ok();
639    writeln!(body, "                Files.createDirectories(out.getParent());").ok();
640    writeln!(
641        body,
642        "                Files.copy(src, out, StandardCopyOption.REPLACE_EXISTING);"
643    )
644    .ok();
645    writeln!(body, "                out.toFile().deleteOnExit();").ok();
646    writeln!(body, "                copied.add(out);").ok();
647    writeln!(body, "            }}").ok();
648    writeln!(body, "        }}").ok();
649    writeln!(body, "        return copied;").ok();
650    writeln!(body, "    }}").ok();
651    writeln!(body).ok();
652    writeln!(
653        body,
654        "    private static Path safeResolve(Path destDir, String relative) throws Exception {{"
655    )
656    .ok();
657    writeln!(
658        body,
659        "        Path normalizedDest = destDir.toAbsolutePath().normalize();"
660    )
661    .ok();
662    writeln!(body, "        Path out = normalizedDest.resolve(relative).normalize();").ok();
663    writeln!(body, "        if (!out.startsWith(normalizedDest)) {{").ok();
664    writeln!(body, "            throw new SecurityException(\"Blocked extracting native file outside destination directory: \" + relative);").ok();
665    writeln!(body, "        }}").ok();
666    writeln!(body, "        return out;").ok();
667    writeln!(body, "    }}").ok();
668    writeln!(body).ok();
669    writeln!(
670        body,
671        "    private static String resolveNativesRid(String osName, String osArch) {{"
672    )
673    .ok();
674    writeln!(body, "        String arch;").ok();
675    writeln!(
676        body,
677        "        if (osArch.contains(\"aarch64\") || osArch.contains(\"arm64\")) {{"
678    )
679    .ok();
680    writeln!(body, "            arch = \"arm64\";").ok();
681    writeln!(
682        body,
683        "        }} else if (osArch.contains(\"x86_64\") || osArch.contains(\"amd64\")) {{"
684    )
685    .ok();
686    writeln!(body, "            arch = \"x86_64\";").ok();
687    writeln!(body, "        }} else {{").ok();
688    writeln!(body, "            arch = osArch.replaceAll(\"[^a-z0-9_]+\", \"\");").ok();
689    writeln!(body, "        }}").ok();
690    writeln!(body).ok();
691    writeln!(body, "        String os;").ok();
692    writeln!(
693        body,
694        "        if (osName.contains(\"mac\") || osName.contains(\"darwin\")) {{"
695    )
696    .ok();
697    writeln!(body, "            os = \"macos\";").ok();
698    writeln!(body, "        }} else if (osName.contains(\"win\")) {{").ok();
699    writeln!(body, "            os = \"windows\";").ok();
700    writeln!(body, "        }} else {{").ok();
701    writeln!(body, "            os = \"linux\";").ok();
702    writeln!(body, "        }}").ok();
703    writeln!(body).ok();
704    writeln!(body, "        return os + \"-\" + arch;").ok();
705    writeln!(body, "    }}").ok();
706    writeln!(body).ok();
707
708    // Generate method handles for free functions.
709    // All functions get handles regardless of is_async — the FFI layer always exposes
710    // synchronous C functions, and the Java async wrapper delegates to the sync method.
711    for func in &api.functions {
712        let ffi_name = format!("{}_{}", prefix, func.name.to_lowercase());
713        let return_layout = gen_ffi_layout(&func.return_type);
714        let param_layouts: Vec<String> = func.params.iter().map(|p| gen_ffi_layout(&p.ty)).collect();
715
716        let layout_str = gen_function_descriptor(&return_layout, &param_layouts);
717
718        let handle_name = format!("{}_{}", prefix.to_uppercase(), func.name.to_uppercase());
719
720        writeln!(
721            body,
722            "    static final MethodHandle {} = LINKER.downcallHandle(",
723            handle_name
724        )
725        .ok();
726        writeln!(body, "        LIB.find(\"{}\").orElseThrow(),", ffi_name).ok();
727        writeln!(body, "        {}", layout_str).ok();
728        writeln!(body, "    );").ok();
729    }
730
731    // free_string handle for releasing FFI-allocated strings
732    {
733        let free_name = format!("{}_free_string", prefix);
734        let handle_name = format!("{}_FREE_STRING", prefix.to_uppercase());
735        writeln!(body).ok();
736        writeln!(
737            body,
738            "    static final MethodHandle {} = LINKER.downcallHandle(",
739            handle_name
740        )
741        .ok();
742        writeln!(body, "        LIB.find(\"{}\").orElseThrow(),", free_name).ok();
743        writeln!(body, "        FunctionDescriptor.ofVoid(ValueLayout.ADDRESS)").ok();
744        writeln!(body, "    );").ok();
745    }
746
747    // Error handling — use the FFI's last_error_code and last_error_context symbols
748    {
749        writeln!(
750            body,
751            "    static final MethodHandle {}_LAST_ERROR_CODE = LINKER.downcallHandle(",
752            prefix.to_uppercase()
753        )
754        .ok();
755        writeln!(body, "        LIB.find(\"{}_last_error_code\").orElseThrow(),", prefix).ok();
756        writeln!(body, "        FunctionDescriptor.of(ValueLayout.JAVA_INT)").ok();
757        writeln!(body, "    );").ok();
758
759        writeln!(
760            body,
761            "    static final MethodHandle {}_LAST_ERROR_CONTEXT = LINKER.downcallHandle(",
762            prefix.to_uppercase()
763        )
764        .ok();
765        writeln!(
766            body,
767            "        LIB.find(\"{}_last_error_context\").orElseThrow(),",
768            prefix
769        )
770        .ok();
771        writeln!(body, "        FunctionDescriptor.of(ValueLayout.ADDRESS)").ok();
772        writeln!(body, "    );").ok();
773    }
774
775    // Track emitted free handles to avoid duplicates (a type may appear both as
776    // a function return type AND as an opaque type).
777    let mut emitted_free_handles: AHashSet<String> = AHashSet::new();
778
779    // Build the set of opaque type names so we can pick the right accessor below.
780    let opaque_type_names: AHashSet<String> = api
781        .types
782        .iter()
783        .filter(|t| t.is_opaque)
784        .map(|t| t.name.clone())
785        .collect();
786
787    // Accessor handles for Named return types (struct pointer → field accessor + free)
788    for func in &api.functions {
789        if let TypeRef::Named(name) = &func.return_type {
790            let type_snake = name.to_snake_case();
791            let type_upper = type_snake.to_uppercase();
792            let is_opaque = opaque_type_names.contains(name.as_str());
793
794            if is_opaque {
795                // Opaque handles: the caller wraps the pointer directly, no JSON needed.
796                // No content accessor is emitted for opaque types.
797            } else {
798                // Non-opaque record types: use _to_json to serialize the full struct to JSON,
799                // which the Java side then deserializes with ObjectMapper.
800                // NOTE: _content returns only the markdown string field, not the full JSON.
801                let to_json_handle = format!("{}_{}_TO_JSON", prefix.to_uppercase(), type_upper);
802                let to_json_ffi = format!("{}_{}_to_json", prefix, type_snake);
803                writeln!(body).ok();
804                writeln!(
805                    body,
806                    "    static final MethodHandle {} = LINKER.downcallHandle(",
807                    to_json_handle
808                )
809                .ok();
810                writeln!(body, "        LIB.find(\"{}\").orElseThrow(),", to_json_ffi).ok();
811                writeln!(
812                    body,
813                    "        FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS)"
814                )
815                .ok();
816                writeln!(body, "    );").ok();
817            }
818
819            // _free: (struct_ptr) -> void
820            let free_handle = format!("{}_{}_FREE", prefix.to_uppercase(), type_upper);
821            let free_ffi = format!("{}_{}_free", prefix, type_snake);
822            if emitted_free_handles.insert(free_handle.clone()) {
823                writeln!(body).ok();
824                writeln!(
825                    body,
826                    "    static final MethodHandle {} = LINKER.downcallHandle(",
827                    free_handle
828                )
829                .ok();
830                writeln!(body, "        LIB.find(\"{}\").orElseThrow(),", free_ffi).ok();
831                writeln!(body, "        FunctionDescriptor.ofVoid(ValueLayout.ADDRESS)").ok();
832                writeln!(body, "    );").ok();
833            }
834        }
835    }
836
837    // FROM_JSON + FREE handles for non-opaque Named types used as parameters.
838    // These allow serializing a Java record to JSON and passing it to the FFI.
839    let mut emitted_from_json_handles: AHashSet<String> = AHashSet::new();
840    for func in &api.functions {
841        for param in &func.params {
842            // Handle both Named and Optional<Named> params
843            let inner_name = match &param.ty {
844                TypeRef::Named(n) => Some(n.clone()),
845                TypeRef::Optional(inner) => {
846                    if let TypeRef::Named(n) = inner.as_ref() {
847                        Some(n.clone())
848                    } else {
849                        None
850                    }
851                }
852                _ => None,
853            };
854            if let Some(name) = inner_name {
855                if !opaque_type_names.contains(name.as_str()) {
856                    let type_snake = name.to_snake_case();
857                    let type_upper = type_snake.to_uppercase();
858
859                    // _from_json: (char*) -> struct_ptr
860                    let from_json_handle = format!("{}_{}_FROM_JSON", prefix.to_uppercase(), type_upper);
861                    let from_json_ffi = format!("{}_{}_from_json", prefix, type_snake);
862                    if emitted_from_json_handles.insert(from_json_handle.clone()) {
863                        writeln!(body).ok();
864                        writeln!(
865                            body,
866                            "    static final MethodHandle {} = LINKER.downcallHandle(",
867                            from_json_handle
868                        )
869                        .ok();
870                        writeln!(body, "        LIB.find(\"{}\").orElseThrow(),", from_json_ffi).ok();
871                        writeln!(
872                            body,
873                            "        FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS)"
874                        )
875                        .ok();
876                        writeln!(body, "    );").ok();
877                    }
878
879                    // _free: (struct_ptr) -> void
880                    let free_handle = format!("{}_{}_FREE", prefix.to_uppercase(), type_upper);
881                    let free_ffi = format!("{}_{}_free", prefix, type_snake);
882                    if emitted_free_handles.insert(free_handle.clone()) {
883                        writeln!(body).ok();
884                        writeln!(
885                            body,
886                            "    static final MethodHandle {} = LINKER.downcallHandle(",
887                            free_handle
888                        )
889                        .ok();
890                        writeln!(body, "        LIB.find(\"{}\").orElseThrow(),", free_ffi).ok();
891                        writeln!(body, "        FunctionDescriptor.ofVoid(ValueLayout.ADDRESS)").ok();
892                        writeln!(body, "    );").ok();
893                    }
894                }
895            }
896        }
897    }
898
899    // Collect builder class names from record types with defaults, so we skip
900    // opaque types that are superseded by a pure-Java builder class.
901    let builder_class_names: AHashSet<String> = api
902        .types
903        .iter()
904        .filter(|t| !t.is_opaque && !t.fields.is_empty() && t.has_default)
905        .map(|t| format!("{}Builder", t.name))
906        .collect();
907
908    // Free handles for opaque types (handle pointer → void)
909    for typ in api.types.iter().filter(|typ| !typ.is_trait) {
910        if typ.is_opaque && !builder_class_names.contains(&typ.name) {
911            let type_snake = typ.name.to_snake_case();
912            let type_upper = type_snake.to_uppercase();
913            let free_handle = format!("{}_{}_FREE", prefix.to_uppercase(), type_upper);
914            let free_ffi = format!("{}_{}_free", prefix, type_snake);
915            if emitted_free_handles.insert(free_handle.clone()) {
916                writeln!(body).ok();
917                writeln!(
918                    body,
919                    "    static final MethodHandle {} = LINKER.downcallHandle(",
920                    free_handle
921                )
922                .ok();
923                writeln!(body, "        LIB.find(\"{}\").orElseThrow(),", free_ffi).ok();
924                writeln!(body, "        FunctionDescriptor.ofVoid(ValueLayout.ADDRESS)").ok();
925                writeln!(body, "    );").ok();
926            }
927        }
928    }
929
930    // Inject visitor FFI method handles when a trait bridge is configured.
931    if has_visitor_bridge {
932        body.push_str(&crate::gen_visitor::gen_native_lib_visitor_handles(prefix));
933    }
934
935    writeln!(body, "}}").ok();
936
937    // Now assemble the file with only the imports that are actually used in the body.
938    let mut out = String::with_capacity(body.len() + 512);
939
940    writeln!(out, "// DO NOT EDIT - auto-generated by alef").ok();
941    writeln!(out, "package {};", package).ok();
942    writeln!(out).ok();
943    if body.contains("Arena") {
944        writeln!(out, "import java.lang.foreign.Arena;").ok();
945    }
946    if body.contains("FunctionDescriptor") {
947        writeln!(out, "import java.lang.foreign.FunctionDescriptor;").ok();
948    }
949    if body.contains("Linker") {
950        writeln!(out, "import java.lang.foreign.Linker;").ok();
951    }
952    if body.contains("MemorySegment") {
953        writeln!(out, "import java.lang.foreign.MemorySegment;").ok();
954    }
955    if body.contains("SymbolLookup") {
956        writeln!(out, "import java.lang.foreign.SymbolLookup;").ok();
957    }
958    if body.contains("ValueLayout") {
959        writeln!(out, "import java.lang.foreign.ValueLayout;").ok();
960    }
961    if body.contains("MethodHandle") {
962        writeln!(out, "import java.lang.invoke.MethodHandle;").ok();
963    }
964    // Imports required by the JAR-extraction native loader (always present).
965    writeln!(out, "import java.net.URL;").ok();
966    writeln!(out, "import java.nio.file.Files;").ok();
967    writeln!(out, "import java.nio.file.Path;").ok();
968    writeln!(out, "import java.nio.file.StandardCopyOption;").ok();
969    writeln!(out, "import java.util.ArrayList;").ok();
970    writeln!(out, "import java.util.Enumeration;").ok();
971    writeln!(out, "import java.util.List;").ok();
972    writeln!(out, "import java.util.jar.JarEntry;").ok();
973    writeln!(out, "import java.util.jar.JarFile;").ok();
974    writeln!(out).ok();
975
976    out.push_str(&body);
977
978    out
979}
980
981// ---------------------------------------------------------------------------
982// Main wrapper class
983// ---------------------------------------------------------------------------
984
985#[allow(clippy::too_many_arguments)]
986fn gen_main_class(
987    api: &ApiSurface,
988    _config: &AlefConfig,
989    package: &str,
990    class_name: &str,
991    prefix: &str,
992    bridge_param_names: &HashSet<String>,
993    bridge_type_aliases: &HashSet<String>,
994    has_visitor_bridge: bool,
995) -> String {
996    // Build the set of opaque type names so we can distinguish opaque handles from records
997    let opaque_types: AHashSet<String> = api
998        .types
999        .iter()
1000        .filter(|t| t.is_opaque)
1001        .map(|t| t.name.clone())
1002        .collect();
1003
1004    // Generate the class body first, then scan it to determine which imports are needed.
1005    let mut body = String::with_capacity(4096);
1006
1007    writeln!(body, "public final class {} {{", class_name).ok();
1008    writeln!(body, "    private {}() {{ }}", class_name).ok();
1009    writeln!(body).ok();
1010
1011    // Generate static methods for free functions
1012    for func in &api.functions {
1013        // Always generate sync method (bridge params stripped from signature)
1014        gen_sync_function_method(
1015            &mut body,
1016            func,
1017            prefix,
1018            class_name,
1019            &opaque_types,
1020            bridge_param_names,
1021            bridge_type_aliases,
1022        );
1023        writeln!(body).ok();
1024
1025        // Also generate async wrapper if marked as async
1026        if func.is_async {
1027            gen_async_wrapper_method(&mut body, func, bridge_param_names, bridge_type_aliases);
1028            writeln!(body).ok();
1029        }
1030    }
1031
1032    // Inject convertWithVisitor when a visitor bridge is configured.
1033    if has_visitor_bridge {
1034        body.push_str(&crate::gen_visitor::gen_convert_with_visitor_method(class_name, prefix));
1035        writeln!(body).ok();
1036    }
1037
1038    // Add helper methods only if they are referenced in the body
1039    gen_helper_methods(&mut body, prefix, class_name);
1040
1041    writeln!(body, "}}").ok();
1042
1043    // Now assemble the file with only the imports that are actually used in the body.
1044    let mut out = String::with_capacity(body.len() + 512);
1045
1046    writeln!(out, "// DO NOT EDIT - auto-generated by alef").ok();
1047    writeln!(out, "package {};", package).ok();
1048    writeln!(out).ok();
1049    if body.contains("Arena") {
1050        writeln!(out, "import java.lang.foreign.Arena;").ok();
1051    }
1052    if body.contains("FunctionDescriptor") {
1053        writeln!(out, "import java.lang.foreign.FunctionDescriptor;").ok();
1054    }
1055    if body.contains("Linker") {
1056        writeln!(out, "import java.lang.foreign.Linker;").ok();
1057    }
1058    if body.contains("MemorySegment") {
1059        writeln!(out, "import java.lang.foreign.MemorySegment;").ok();
1060    }
1061    if body.contains("SymbolLookup") {
1062        writeln!(out, "import java.lang.foreign.SymbolLookup;").ok();
1063    }
1064    if body.contains("ValueLayout") {
1065        writeln!(out, "import java.lang.foreign.ValueLayout;").ok();
1066    }
1067    if body.contains("List<") {
1068        writeln!(out, "import java.util.List;").ok();
1069    }
1070    if body.contains("Map<") {
1071        writeln!(out, "import java.util.Map;").ok();
1072    }
1073    if body.contains("Optional<") {
1074        writeln!(out, "import java.util.Optional;").ok();
1075    }
1076    if body.contains("HashMap<") || body.contains("new HashMap") {
1077        writeln!(out, "import java.util.HashMap;").ok();
1078    }
1079    if body.contains("CompletableFuture") {
1080        writeln!(out, "import java.util.concurrent.CompletableFuture;").ok();
1081    }
1082    if body.contains("CompletionException") {
1083        writeln!(out, "import java.util.concurrent.CompletionException;").ok();
1084    }
1085    // Only import the short name `ObjectMapper` when it's used as a type reference (not just via
1086    // `createObjectMapper()` which uses fully qualified names internally).
1087    // Check for " ObjectMapper" (space before) which indicates use as a type, not a method name suffix.
1088    if body.contains(" ObjectMapper") {
1089        writeln!(out, "import com.fasterxml.jackson.databind.ObjectMapper;").ok();
1090    }
1091    writeln!(out).ok();
1092
1093    out.push_str(&body);
1094
1095    out
1096}
1097
1098fn is_bridge_param_java(
1099    param: &alef_core::ir::ParamDef,
1100    bridge_param_names: &HashSet<String>,
1101    bridge_type_aliases: &HashSet<String>,
1102) -> bool {
1103    if bridge_param_names.contains(param.name.as_str()) {
1104        return true;
1105    }
1106    let type_name = match &param.ty {
1107        TypeRef::Named(n) => Some(n.as_str()),
1108        TypeRef::Optional(inner) => {
1109            if let TypeRef::Named(n) = inner.as_ref() {
1110                Some(n.as_str())
1111            } else {
1112                None
1113            }
1114        }
1115        _ => None,
1116    };
1117    type_name.is_some_and(|n| bridge_type_aliases.contains(n))
1118}
1119
1120fn gen_sync_function_method(
1121    out: &mut String,
1122    func: &FunctionDef,
1123    prefix: &str,
1124    class_name: &str,
1125    opaque_types: &AHashSet<String>,
1126    bridge_param_names: &HashSet<String>,
1127    bridge_type_aliases: &HashSet<String>,
1128) {
1129    // Exclude bridge params from the public Java signature.
1130    let params: Vec<String> = func
1131        .params
1132        .iter()
1133        .filter(|p| !is_bridge_param_java(p, bridge_param_names, bridge_type_aliases))
1134        .map(|p| {
1135            let ptype = java_type(&p.ty);
1136            format!("{} {}", ptype, to_java_name(&p.name))
1137        })
1138        .collect();
1139
1140    let return_type = java_type(&func.return_type);
1141
1142    writeln!(
1143        out,
1144        "    public static {} {}({}) throws {}Exception {{",
1145        return_type,
1146        to_java_name(&func.name),
1147        params.join(", "),
1148        class_name
1149    )
1150    .ok();
1151
1152    writeln!(out, "        try (var arena = Arena.ofConfined()) {{").ok();
1153
1154    // Collect non-opaque Named params that need FFI pointer cleanup after the call.
1155    // These are Rust-allocated by _from_json and must be freed with _free.
1156    // Bridge params are excluded — they are passed as NULL.
1157    let ffi_ptr_params: Vec<(String, String)> = func
1158        .params
1159        .iter()
1160        .filter(|p| !is_bridge_param_java(p, bridge_param_names, bridge_type_aliases))
1161        .filter_map(|p| {
1162            let inner_name = match &p.ty {
1163                TypeRef::Named(n) if !opaque_types.contains(n.as_str()) => Some(n.clone()),
1164                TypeRef::Optional(inner) => {
1165                    if let TypeRef::Named(n) = inner.as_ref() {
1166                        if !opaque_types.contains(n.as_str()) {
1167                            Some(n.clone())
1168                        } else {
1169                            None
1170                        }
1171                    } else {
1172                        None
1173                    }
1174                }
1175                _ => None,
1176            };
1177            inner_name.map(|type_name| {
1178                let cname = "c".to_string() + &to_java_name(&p.name);
1179                let type_snake = type_name.to_snake_case();
1180                let free_handle = format!("NativeLib.{}_{}_FREE", prefix.to_uppercase(), type_snake.to_uppercase());
1181                (cname, free_handle)
1182            })
1183        })
1184        .collect();
1185
1186    // Marshal non-bridge parameters (use camelCase Java names)
1187    for param in &func.params {
1188        if is_bridge_param_java(param, bridge_param_names, bridge_type_aliases) {
1189            continue;
1190        }
1191        marshal_param_to_ffi(out, &to_java_name(&param.name), &param.ty, opaque_types, prefix);
1192    }
1193
1194    // Call FFI
1195    let ffi_handle = format!("NativeLib.{}_{}", prefix.to_uppercase(), func.name.to_uppercase());
1196
1197    // Build call args: bridge params get MemorySegment.NULL, others are marshalled normally.
1198    let call_args: Vec<String> = func
1199        .params
1200        .iter()
1201        .map(|p| {
1202            if is_bridge_param_java(p, bridge_param_names, bridge_type_aliases) {
1203                "MemorySegment.NULL".to_string()
1204            } else {
1205                ffi_param_name(&to_java_name(&p.name), &p.ty, opaque_types)
1206            }
1207        })
1208        .collect();
1209
1210    // Emit a helper closure to free FFI-allocated param pointers (e.g. options created by _from_json)
1211    let emit_ffi_ptr_cleanup = |out: &mut String| {
1212        for (cname, free_handle) in &ffi_ptr_params {
1213            writeln!(out, "            if (!{}.equals(MemorySegment.NULL)) {{", cname).ok();
1214            writeln!(out, "                {}.invoke({});", free_handle, cname).ok();
1215            writeln!(out, "            }}").ok();
1216        }
1217    };
1218
1219    if matches!(func.return_type, TypeRef::Unit) {
1220        writeln!(out, "            {}.invoke({});", ffi_handle, call_args.join(", ")).ok();
1221        emit_ffi_ptr_cleanup(out);
1222        writeln!(out, "        }} catch (Throwable e) {{").ok();
1223        writeln!(
1224            out,
1225            "            throw new {}Exception(\"FFI call failed\", e);",
1226            class_name
1227        )
1228        .ok();
1229        writeln!(out, "        }}").ok();
1230    } else if is_ffi_string_return(&func.return_type) {
1231        let free_handle = format!("NativeLib.{}_FREE_STRING", prefix.to_uppercase());
1232        writeln!(
1233            out,
1234            "            var resultPtr = (MemorySegment) {}.invoke({});",
1235            ffi_handle,
1236            call_args.join(", ")
1237        )
1238        .ok();
1239        emit_ffi_ptr_cleanup(out);
1240        writeln!(out, "            if (resultPtr.equals(MemorySegment.NULL)) {{").ok();
1241        writeln!(out, "                checkLastError();").ok();
1242        writeln!(out, "                return null;").ok();
1243        writeln!(out, "            }}").ok();
1244        writeln!(
1245            out,
1246            "            String result = resultPtr.reinterpret(Long.MAX_VALUE).getString(0);"
1247        )
1248        .ok();
1249        writeln!(out, "            {}.invoke(resultPtr);", free_handle).ok();
1250        writeln!(out, "            return result;").ok();
1251        writeln!(out, "        }} catch (Throwable e) {{").ok();
1252        writeln!(
1253            out,
1254            "            throw new {}Exception(\"FFI call failed\", e);",
1255            class_name
1256        )
1257        .ok();
1258        writeln!(out, "        }}").ok();
1259    } else if matches!(func.return_type, TypeRef::Named(_)) {
1260        // Named return types: FFI returns a struct pointer.
1261        let return_type_name = match &func.return_type {
1262            TypeRef::Named(name) => name,
1263            _ => unreachable!(),
1264        };
1265        let is_opaque = opaque_types.contains(return_type_name.as_str());
1266
1267        writeln!(
1268            out,
1269            "            var resultPtr = (MemorySegment) {}.invoke({});",
1270            ffi_handle,
1271            call_args.join(", ")
1272        )
1273        .ok();
1274        emit_ffi_ptr_cleanup(out);
1275        writeln!(out, "            if (resultPtr.equals(MemorySegment.NULL)) {{").ok();
1276        writeln!(out, "                checkLastError();").ok();
1277        writeln!(out, "                return null;").ok();
1278        writeln!(out, "            }}").ok();
1279
1280        if is_opaque {
1281            // Opaque handles: wrap the raw pointer directly, caller owns and will close()
1282            writeln!(out, "            return new {}(resultPtr);", return_type_name).ok();
1283        } else {
1284            // Record types: use _to_json to serialize the full struct to JSON, then deserialize.
1285            // NOTE: _content only returns the markdown string field, not a full JSON object.
1286            let type_snake = return_type_name.to_snake_case();
1287            let free_handle = format!("NativeLib.{}_{}_FREE", prefix.to_uppercase(), type_snake.to_uppercase());
1288            let to_json_handle = format!(
1289                "NativeLib.{}_{}_TO_JSON",
1290                prefix.to_uppercase(),
1291                type_snake.to_uppercase()
1292            );
1293            writeln!(
1294                out,
1295                "            var jsonPtr = (MemorySegment) {}.invoke(resultPtr);",
1296                to_json_handle
1297            )
1298            .ok();
1299            writeln!(out, "            {}.invoke(resultPtr);", free_handle).ok();
1300            writeln!(out, "            if (jsonPtr.equals(MemorySegment.NULL)) {{").ok();
1301            writeln!(out, "                checkLastError();").ok();
1302            writeln!(out, "                return null;").ok();
1303            writeln!(out, "            }}").ok();
1304            writeln!(
1305                out,
1306                "            String json = jsonPtr.reinterpret(Long.MAX_VALUE).getString(0);"
1307            )
1308            .ok();
1309            writeln!(
1310                out,
1311                "            NativeLib.{}_FREE_STRING.invoke(jsonPtr);",
1312                prefix.to_uppercase()
1313            )
1314            .ok();
1315            writeln!(
1316                out,
1317                "            return createObjectMapper().readValue(json, {}.class);",
1318                return_type_name
1319            )
1320            .ok();
1321        }
1322
1323        writeln!(out, "        }} catch (Throwable e) {{").ok();
1324        writeln!(
1325            out,
1326            "            throw new {}Exception(\"FFI call failed\", e);",
1327            class_name
1328        )
1329        .ok();
1330        writeln!(out, "        }}").ok();
1331    } else if matches!(func.return_type, TypeRef::Vec(_)) {
1332        // Vec return types: FFI returns a JSON string pointer; deserialize into List<T>.
1333        let free_handle = format!("NativeLib.{}_FREE_STRING", prefix.to_uppercase());
1334        writeln!(
1335            out,
1336            "            var resultPtr = (MemorySegment) {}.invoke({});",
1337            ffi_handle,
1338            call_args.join(", ")
1339        )
1340        .ok();
1341        emit_ffi_ptr_cleanup(out);
1342        writeln!(out, "            if (resultPtr.equals(MemorySegment.NULL)) {{").ok();
1343        writeln!(out, "                return java.util.List.of();").ok();
1344        writeln!(out, "            }}").ok();
1345        writeln!(
1346            out,
1347            "            String json = resultPtr.reinterpret(Long.MAX_VALUE).getString(0);"
1348        )
1349        .ok();
1350        writeln!(out, "            {}.invoke(resultPtr);", free_handle).ok();
1351        // Determine the element type for deserialization
1352        let element_type = match &func.return_type {
1353            TypeRef::Vec(inner) => java_type(inner),
1354            _ => unreachable!(),
1355        };
1356        writeln!(
1357            out,
1358            "            return createObjectMapper().readValue(json, new com.fasterxml.jackson.core.type.TypeReference<java.util.List<{}>>() {{ }});",
1359            element_type
1360        )
1361        .ok();
1362        writeln!(out, "        }} catch (Throwable e) {{").ok();
1363        writeln!(
1364            out,
1365            "            throw new {}Exception(\"FFI call failed\", e);",
1366            class_name
1367        )
1368        .ok();
1369        writeln!(out, "        }}").ok();
1370    } else {
1371        writeln!(
1372            out,
1373            "            var primitiveResult = ({}) {}.invoke({});",
1374            java_ffi_return_cast(&func.return_type),
1375            ffi_handle,
1376            call_args.join(", ")
1377        )
1378        .ok();
1379        emit_ffi_ptr_cleanup(out);
1380        writeln!(out, "            return primitiveResult;").ok();
1381        writeln!(out, "        }} catch (Throwable e) {{").ok();
1382        writeln!(
1383            out,
1384            "            throw new {}Exception(\"FFI call failed\", e);",
1385            class_name
1386        )
1387        .ok();
1388        writeln!(out, "        }}").ok();
1389    }
1390
1391    writeln!(out, "    }}").ok();
1392}
1393
1394fn gen_async_wrapper_method(
1395    out: &mut String,
1396    func: &FunctionDef,
1397    bridge_param_names: &HashSet<String>,
1398    bridge_type_aliases: &HashSet<String>,
1399) {
1400    let params: Vec<String> = func
1401        .params
1402        .iter()
1403        .filter(|p| !is_bridge_param_java(p, bridge_param_names, bridge_type_aliases))
1404        .map(|p| {
1405            let ptype = java_type(&p.ty);
1406            format!("{} {}", ptype, to_java_name(&p.name))
1407        })
1408        .collect();
1409
1410    let return_type = match &func.return_type {
1411        TypeRef::Unit => "Void".to_string(),
1412        other => java_boxed_type(other).to_string(),
1413    };
1414
1415    let sync_method_name = to_java_name(&func.name);
1416    let async_method_name = format!("{}Async", sync_method_name);
1417    let param_names: Vec<String> = func
1418        .params
1419        .iter()
1420        .filter(|p| !is_bridge_param_java(p, bridge_param_names, bridge_type_aliases))
1421        .map(|p| to_java_name(&p.name))
1422        .collect();
1423
1424    writeln!(
1425        out,
1426        "    public static CompletableFuture<{}> {}({}) {{",
1427        return_type,
1428        async_method_name,
1429        params.join(", ")
1430    )
1431    .ok();
1432    writeln!(out, "        return CompletableFuture.supplyAsync(() -> {{").ok();
1433    writeln!(out, "            try {{").ok();
1434    writeln!(
1435        out,
1436        "                return {}({});",
1437        sync_method_name,
1438        param_names.join(", ")
1439    )
1440    .ok();
1441    writeln!(out, "            }} catch (Throwable e) {{").ok();
1442    writeln!(out, "                throw new CompletionException(e);").ok();
1443    writeln!(out, "            }}").ok();
1444    writeln!(out, "        }});").ok();
1445    writeln!(out, "    }}").ok();
1446}
1447
1448// ---------------------------------------------------------------------------
1449// Exception class
1450// ---------------------------------------------------------------------------
1451
1452fn gen_exception_class(package: &str, class_name: &str) -> String {
1453    let mut out = String::with_capacity(512);
1454
1455    writeln!(out, "// DO NOT EDIT - auto-generated by alef").ok();
1456    writeln!(out, "package {};", package).ok();
1457    writeln!(out).ok();
1458
1459    writeln!(out, "public class {}Exception extends Exception {{", class_name).ok();
1460    writeln!(out, "    private final int code;").ok();
1461    writeln!(out).ok();
1462    writeln!(out, "    public {}Exception(int code, String message) {{", class_name).ok();
1463    writeln!(out, "        super(message);").ok();
1464    writeln!(out, "        this.code = code;").ok();
1465    writeln!(out, "    }}").ok();
1466    writeln!(out).ok();
1467    writeln!(
1468        out,
1469        "    public {}Exception(String message, Throwable cause) {{",
1470        class_name
1471    )
1472    .ok();
1473    writeln!(out, "        super(message, cause);").ok();
1474    writeln!(out, "        this.code = -1;").ok();
1475    writeln!(out, "    }}").ok();
1476    writeln!(out).ok();
1477    writeln!(out, "    public int getCode() {{").ok();
1478    writeln!(out, "        return code;").ok();
1479    writeln!(out, "    }}").ok();
1480    writeln!(out, "}}").ok();
1481
1482    out
1483}
1484
1485// ---------------------------------------------------------------------------
1486// High-level facade class (public API)
1487// ---------------------------------------------------------------------------
1488
1489#[allow(clippy::too_many_arguments)]
1490fn gen_facade_class(
1491    api: &ApiSurface,
1492    package: &str,
1493    public_class: &str,
1494    raw_class: &str,
1495    _prefix: &str,
1496    bridge_param_names: &HashSet<String>,
1497    bridge_type_aliases: &HashSet<String>,
1498    has_visitor_bridge: bool,
1499) -> String {
1500    let mut body = String::with_capacity(4096);
1501
1502    writeln!(body, "public final class {} {{", public_class).ok();
1503    writeln!(body, "    private {}() {{ }}", public_class).ok();
1504    writeln!(body).ok();
1505
1506    // Generate static methods for free functions
1507    for func in &api.functions {
1508        // Sync method — bridge params stripped from public signature
1509        let params: Vec<String> = func
1510            .params
1511            .iter()
1512            .filter(|p| !is_bridge_param_java(p, bridge_param_names, bridge_type_aliases))
1513            .map(|p| {
1514                let ptype = java_type(&p.ty);
1515                format!("{} {}", ptype, to_java_name(&p.name))
1516            })
1517            .collect();
1518
1519        let return_type = java_type(&func.return_type);
1520
1521        if !func.doc.is_empty() {
1522            writeln!(body, "    /**").ok();
1523            for line in func.doc.lines() {
1524                writeln!(body, "     * {}", line).ok();
1525            }
1526            writeln!(body, "     */").ok();
1527        }
1528
1529        writeln!(
1530            body,
1531            "    public static {} {}({}) throws {}Exception {{",
1532            return_type,
1533            to_java_name(&func.name),
1534            params.join(", "),
1535            raw_class
1536        )
1537        .ok();
1538
1539        // Null checks for required non-bridge parameters
1540        for param in &func.params {
1541            if !param.optional && !is_bridge_param_java(param, bridge_param_names, bridge_type_aliases) {
1542                let pname = to_java_name(&param.name);
1543                writeln!(
1544                    body,
1545                    "        java.util.Objects.requireNonNull({}, \"{} must not be null\");",
1546                    pname, pname
1547                )
1548                .ok();
1549            }
1550        }
1551
1552        // Delegate to raw FFI class — bridge params are stripped from the raw class
1553        // signature, so we must exclude them entirely (not pass null) to match the
1554        // raw class's parameter count.
1555        let call_args: Vec<String> = func
1556            .params
1557            .iter()
1558            .filter(|p| !is_bridge_param_java(p, bridge_param_names, bridge_type_aliases))
1559            .map(|p| to_java_name(&p.name))
1560            .collect();
1561
1562        if matches!(func.return_type, TypeRef::Unit) {
1563            writeln!(
1564                body,
1565                "        {}.{}({});",
1566                raw_class,
1567                to_java_name(&func.name),
1568                call_args.join(", ")
1569            )
1570            .ok();
1571        } else {
1572            writeln!(
1573                body,
1574                "        return {}.{}({});",
1575                raw_class,
1576                to_java_name(&func.name),
1577                call_args.join(", ")
1578            )
1579            .ok();
1580        }
1581
1582        writeln!(body, "    }}").ok();
1583        writeln!(body).ok();
1584
1585        // Generate overload without optional params (convenience method).
1586        // Only non-bridge params are considered here.
1587        let has_optional = func
1588            .params
1589            .iter()
1590            .any(|p| p.optional && !is_bridge_param_java(p, bridge_param_names, bridge_type_aliases));
1591        if has_optional {
1592            let required_params: Vec<String> = func
1593                .params
1594                .iter()
1595                .filter(|p| !p.optional && !is_bridge_param_java(p, bridge_param_names, bridge_type_aliases))
1596                .map(|p| {
1597                    let ptype = java_type(&p.ty);
1598                    format!("{} {}", ptype, to_java_name(&p.name))
1599                })
1600                .collect();
1601
1602            writeln!(
1603                body,
1604                "    public static {} {}({}) throws {}Exception {{",
1605                return_type,
1606                to_java_name(&func.name),
1607                required_params.join(", "),
1608                raw_class
1609            )
1610            .ok();
1611
1612            // Build call to raw class: bridge params are excluded (stripped from raw
1613            // class signature), optional params passed as null.
1614            let full_args: Vec<String> = func
1615                .params
1616                .iter()
1617                .filter(|p| !is_bridge_param_java(p, bridge_param_names, bridge_type_aliases))
1618                .map(|p| {
1619                    if p.optional {
1620                        "null".to_string()
1621                    } else {
1622                        to_java_name(&p.name)
1623                    }
1624                })
1625                .collect();
1626
1627            if matches!(func.return_type, TypeRef::Unit) {
1628                writeln!(
1629                    body,
1630                    "        {}.{}({});",
1631                    raw_class,
1632                    to_java_name(&func.name),
1633                    full_args.join(", ")
1634                )
1635                .ok();
1636            } else {
1637                writeln!(
1638                    body,
1639                    "        return {}.{}({});",
1640                    raw_class,
1641                    to_java_name(&func.name),
1642                    full_args.join(", ")
1643                )
1644                .ok();
1645            }
1646
1647            writeln!(body, "    }}").ok();
1648            writeln!(body).ok();
1649        }
1650    }
1651
1652    // Expose convertWithVisitor in the public facade when visitor bridge is configured.
1653    if has_visitor_bridge {
1654        writeln!(body, "    /**").ok();
1655        writeln!(
1656            body,
1657            "     * Convert HTML to Markdown, invoking visitor callbacks during processing."
1658        )
1659        .ok();
1660        writeln!(body, "     */").ok();
1661        writeln!(
1662            body,
1663            "    public static ConversionResult convertWithVisitor(String html, ConversionOptions options, Visitor visitor)"
1664        )
1665        .ok();
1666        writeln!(body, "            throws {}Exception {{", raw_class).ok();
1667        writeln!(
1668            body,
1669            "        return {}.convertWithVisitor(html, options, visitor);",
1670            raw_class
1671        )
1672        .ok();
1673        writeln!(body, "    }}").ok();
1674        writeln!(body).ok();
1675    }
1676
1677    writeln!(body, "}}").ok();
1678
1679    // Now assemble the file with imports
1680    let mut out = String::with_capacity(body.len() + 512);
1681
1682    writeln!(out, "// DO NOT EDIT - auto-generated by alef").ok();
1683    writeln!(out, "package {};", package).ok();
1684
1685    // Check what imports are needed based on content
1686    let has_list = body.contains("List<");
1687    let has_map = body.contains("Map<");
1688    let has_optional = body.contains("Optional<");
1689    let has_imports = has_list || has_map || has_optional;
1690
1691    if has_imports {
1692        writeln!(out).ok();
1693        if has_list {
1694            writeln!(out, "import java.util.List;").ok();
1695        }
1696        if has_map {
1697            writeln!(out, "import java.util.Map;").ok();
1698        }
1699        if has_optional {
1700            writeln!(out, "import java.util.Optional;").ok();
1701        }
1702    }
1703
1704    writeln!(out).ok();
1705    out.push_str(&body);
1706
1707    out
1708}
1709
1710// ---------------------------------------------------------------------------
1711// Opaque handle classes
1712// ---------------------------------------------------------------------------
1713
1714fn gen_opaque_handle_class(package: &str, typ: &TypeDef, prefix: &str) -> String {
1715    let mut out = String::with_capacity(1024);
1716    let class_name = &typ.name;
1717    let type_snake = class_name.to_snake_case();
1718
1719    writeln!(out, "// DO NOT EDIT - auto-generated by alef").ok();
1720    writeln!(out, "package {};", package).ok();
1721    writeln!(out).ok();
1722    writeln!(out, "import java.lang.foreign.MemorySegment;").ok();
1723    writeln!(out).ok();
1724
1725    if !typ.doc.is_empty() {
1726        writeln!(out, "/**").ok();
1727        for line in typ.doc.lines() {
1728            writeln!(out, " * {}", line).ok();
1729        }
1730        writeln!(out, " */").ok();
1731    }
1732
1733    writeln!(out, "public class {} implements AutoCloseable {{", class_name).ok();
1734    writeln!(out, "    private final MemorySegment handle;").ok();
1735    writeln!(out).ok();
1736    writeln!(out, "    {}(MemorySegment handle) {{", class_name).ok();
1737    writeln!(out, "        this.handle = handle;").ok();
1738    writeln!(out, "    }}").ok();
1739    writeln!(out).ok();
1740    writeln!(out, "    MemorySegment handle() {{").ok();
1741    writeln!(out, "        return this.handle;").ok();
1742    writeln!(out, "    }}").ok();
1743    writeln!(out).ok();
1744    writeln!(out, "    @Override").ok();
1745    writeln!(out, "    public void close() {{").ok();
1746    writeln!(
1747        out,
1748        "        if (handle != null && !handle.equals(MemorySegment.NULL)) {{"
1749    )
1750    .ok();
1751    writeln!(out, "            try {{").ok();
1752    writeln!(
1753        out,
1754        "                NativeLib.{}_{}_FREE.invoke(handle);",
1755        prefix.to_uppercase(),
1756        type_snake.to_uppercase()
1757    )
1758    .ok();
1759    writeln!(out, "            }} catch (Throwable e) {{").ok();
1760    writeln!(
1761        out,
1762        "                throw new RuntimeException(\"Failed to free {}: \" + e.getMessage(), e);",
1763        class_name
1764    )
1765    .ok();
1766    writeln!(out, "            }}").ok();
1767    writeln!(out, "        }}").ok();
1768    writeln!(out, "    }}").ok();
1769    writeln!(out, "}}").ok();
1770
1771    out
1772}
1773
1774// ---------------------------------------------------------------------------
1775// Record types (Java records)
1776// ---------------------------------------------------------------------------
1777
1778/// Emit a Javadoc comment block into `out` at the given indentation level.
1779///
1780/// `indent` is the leading whitespace prepended to each line (e.g. `""` for
1781/// top-level declarations, `"    "` for class members).  Does nothing when
1782/// `doc` is empty.
1783fn emit_javadoc(out: &mut String, doc: &str, indent: &str) {
1784    if doc.is_empty() {
1785        return;
1786    }
1787    writeln!(out, "{indent}/**").ok();
1788    for line in doc.lines() {
1789        if line.is_empty() {
1790            writeln!(out, "{indent} *").ok();
1791        } else {
1792            let escaped = escape_javadoc_line(line);
1793            writeln!(out, "{indent} * {escaped}").ok();
1794        }
1795    }
1796    writeln!(out, "{indent} */").ok();
1797}
1798
1799/// Maximum line length before splitting record fields across multiple lines.
1800/// Checkstyle enforces 120 chars; we split at 100 to leave headroom for indentation.
1801const RECORD_LINE_WRAP_THRESHOLD: usize = 100;
1802
1803fn gen_record_type(package: &str, typ: &TypeDef, complex_enums: &AHashSet<String>, lang_rename_all: &str) -> String {
1804    // Generate the record body first, then scan for needed imports.
1805    // For each field, if the language uses camelCase but the JSON key is snake_case
1806    // (the Rust default), annotate with @JsonProperty so Jackson maps correctly.
1807    // Also collect per-field doc strings for Javadoc emission.
1808    let (field_list, field_docs): (Vec<String>, Vec<String>) = typ
1809        .fields
1810        .iter()
1811        .map(|f| {
1812            // Complex enums (tagged unions with data) can't be simple Java enums.
1813            // Use Object for flexible Jackson deserialization.
1814            let is_complex = matches!(&f.ty, TypeRef::Named(n) if complex_enums.contains(n.as_str()));
1815            let ftype = if is_complex {
1816                "Object".to_string()
1817            } else if f.optional {
1818                format!("Optional<{}>", java_boxed_type(&f.ty))
1819            } else {
1820                java_type(&f.ty).to_string()
1821            };
1822            let jname = safe_java_field_name(&f.name);
1823            // When the language convention is camelCase but the JSON wire format uses
1824            // snake_case (the Rust/serde default), add an explicit @JsonProperty annotation
1825            // so Jackson serialises/deserialises using the correct snake_case key.
1826            let decl = if lang_rename_all == "camelCase" && f.name.contains('_') {
1827                format!("@JsonProperty(\"{}\") {} {}", f.name, ftype, jname)
1828            } else {
1829                format!("{} {}", ftype, jname)
1830            };
1831            (decl, f.doc.clone())
1832        })
1833        .unzip();
1834
1835    // Build the single-line form to check length and scan for imports.
1836    // Doc strings are intentionally excluded from this check so the threshold
1837    // stays stable regardless of documentation presence.
1838    let single_line = format!("public record {}({}) {{ }}", typ.name, field_list.join(", "));
1839
1840    // Build the actual record declaration, splitting across lines if too long.
1841    let mut record_block = String::new();
1842    emit_javadoc(&mut record_block, &typ.doc, "");
1843    if single_line.len() > RECORD_LINE_WRAP_THRESHOLD && field_list.len() > 1 {
1844        writeln!(record_block, "public record {}(", typ.name).ok();
1845        for (i, (field, doc)) in field_list.iter().zip(field_docs.iter()).enumerate() {
1846            let comma = if i < field_list.len() - 1 { "," } else { "" };
1847            if !doc.is_empty() {
1848                // Inline single-line doc for record components in multi-line form.
1849                let doc_summary = escape_javadoc_line(doc.lines().next().unwrap_or("").trim());
1850                writeln!(record_block, "    /** {doc_summary} */").ok();
1851            }
1852            writeln!(record_block, "    {}{}", field, comma).ok();
1853        }
1854        writeln!(record_block, ") {{").ok();
1855    } else {
1856        writeln!(record_block, "public record {}({}) {{", typ.name, field_list.join(", ")).ok();
1857    }
1858
1859    // Add builder() factory method if type has defaults
1860    if typ.has_default {
1861        writeln!(record_block, "    public static {}Builder builder() {{", typ.name).ok();
1862        writeln!(record_block, "        return new {}Builder();", typ.name).ok();
1863        writeln!(record_block, "    }}").ok();
1864    }
1865
1866    writeln!(record_block, "}}").ok();
1867
1868    // Scan the single-line form to determine which imports are needed
1869    let needs_json_property = field_list.iter().any(|f| f.contains("@JsonProperty("));
1870    let mut out = String::with_capacity(record_block.len() + 512);
1871    writeln!(out, "// DO NOT EDIT - auto-generated by alef").ok();
1872    writeln!(out, "package {};", package).ok();
1873    writeln!(out).ok();
1874    if single_line.contains("List<") {
1875        writeln!(out, "import java.util.List;").ok();
1876    }
1877    if single_line.contains("Map<") {
1878        writeln!(out, "import java.util.Map;").ok();
1879    }
1880    if single_line.contains("Optional<") {
1881        writeln!(out, "import java.util.Optional;").ok();
1882    }
1883    if needs_json_property {
1884        writeln!(out, "import com.fasterxml.jackson.annotation.JsonProperty;").ok();
1885    }
1886    writeln!(out).ok();
1887    write!(out, "{}", record_block).ok();
1888
1889    out
1890}
1891
1892// ---------------------------------------------------------------------------
1893// Enum classes
1894// ---------------------------------------------------------------------------
1895
1896/// Apply a serde `rename_all` strategy to a variant name for Java codegen.
1897fn java_apply_rename_all(name: &str, rename_all: Option<&str>) -> String {
1898    match rename_all {
1899        Some("snake_case") => name.to_snake_case(),
1900        Some("camelCase") => name.to_lower_camel_case(),
1901        Some("PascalCase") => name.to_pascal_case(),
1902        Some("SCREAMING_SNAKE_CASE") => name.to_snake_case().to_uppercase(),
1903        Some("lowercase") => name.to_lowercase(),
1904        Some("UPPERCASE") => name.to_uppercase(),
1905        _ => name.to_lowercase(),
1906    }
1907}
1908
1909fn gen_enum_class(package: &str, enum_def: &EnumDef) -> String {
1910    let has_data_variants = enum_def.variants.iter().any(|v| !v.fields.is_empty());
1911
1912    // Tagged union: enum has a serde tag AND data variants → generate sealed interface hierarchy
1913    if enum_def.serde_tag.is_some() && has_data_variants {
1914        return gen_java_tagged_union(package, enum_def);
1915    }
1916
1917    let mut out = String::with_capacity(1024);
1918
1919    writeln!(out, "// DO NOT EDIT - auto-generated by alef").ok();
1920    writeln!(out, "package {};", package).ok();
1921    writeln!(out).ok();
1922    writeln!(out, "import com.fasterxml.jackson.annotation.JsonCreator;").ok();
1923    writeln!(out, "import com.fasterxml.jackson.annotation.JsonValue;").ok();
1924    writeln!(out).ok();
1925
1926    emit_javadoc(&mut out, &enum_def.doc, "");
1927    writeln!(out, "public enum {} {{", enum_def.name).ok();
1928
1929    for (i, variant) in enum_def.variants.iter().enumerate() {
1930        let comma = if i < enum_def.variants.len() - 1 { "," } else { ";" };
1931        // Use serde_rename if available, otherwise apply rename_all strategy
1932        let json_name = variant
1933            .serde_rename
1934            .clone()
1935            .unwrap_or_else(|| java_apply_rename_all(&variant.name, enum_def.serde_rename_all.as_deref()));
1936        if !variant.doc.is_empty() {
1937            let doc_summary = escape_javadoc_line(variant.doc.lines().next().unwrap_or("").trim());
1938            writeln!(out, "    /** {doc_summary} */").ok();
1939        }
1940        writeln!(out, "    {}(\"{}\"){}", variant.name, json_name, comma).ok();
1941    }
1942
1943    writeln!(out).ok();
1944    writeln!(out, "    private final String value;").ok();
1945    writeln!(out).ok();
1946    writeln!(out, "    {}(String value) {{", enum_def.name).ok();
1947    writeln!(out, "        this.value = value;").ok();
1948    writeln!(out, "    }}").ok();
1949    writeln!(out).ok();
1950    writeln!(out, "    @JsonValue").ok();
1951    writeln!(out, "    public String getValue() {{").ok();
1952    writeln!(out, "        return value;").ok();
1953    writeln!(out, "    }}").ok();
1954    writeln!(out).ok();
1955    writeln!(out, "    @JsonCreator").ok();
1956    writeln!(out, "    public static {} fromValue(String value) {{", enum_def.name).ok();
1957    writeln!(out, "        for ({} e : values()) {{", enum_def.name).ok();
1958    writeln!(out, "            if (e.value.equalsIgnoreCase(value)) {{").ok();
1959    writeln!(out, "                return e;").ok();
1960    writeln!(out, "            }}").ok();
1961    writeln!(out, "        }}").ok();
1962    writeln!(
1963        out,
1964        "        throw new IllegalArgumentException(\"Unknown value: \" + value);"
1965    )
1966    .ok();
1967    writeln!(out, "    }}").ok();
1968
1969    writeln!(out, "}}").ok();
1970
1971    out
1972}
1973
1974/// Generate a Java sealed interface hierarchy for internally tagged enums.
1975///
1976/// Maps `#[serde(tag = "type_field", rename_all = "snake_case")]` Rust enums to
1977/// `@JsonTypeInfo` / `@JsonSubTypes` Java sealed interfaces with record implementations.
1978fn gen_java_tagged_union(package: &str, enum_def: &EnumDef) -> String {
1979    let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
1980
1981    // Collect variant names to detect Java type name conflicts.
1982    // If a variant is named "List", "Map", or "Optional", using those type names
1983    // inside the sealed interface would refer to the nested record, not java.util.*.
1984    // We use fully qualified names in that case.
1985    let variant_names: std::collections::HashSet<&str> = enum_def.variants.iter().map(|v| v.name.as_str()).collect();
1986    let optional_type = if variant_names.contains("Optional") {
1987        "java.util.Optional"
1988    } else {
1989        "Optional"
1990    };
1991
1992    // @JsonProperty is only needed for variants with named (non-tuple) fields.
1993    let needs_json_property = enum_def
1994        .variants
1995        .iter()
1996        .any(|v| v.fields.iter().any(|f| !is_tuple_field_name(&f.name)));
1997
1998    let mut out = String::with_capacity(2048);
1999    writeln!(out, "// DO NOT EDIT - auto-generated by alef").ok();
2000    writeln!(out, "package {};", package).ok();
2001    writeln!(out).ok();
2002    if needs_json_property {
2003        writeln!(out, "import com.fasterxml.jackson.annotation.JsonProperty;").ok();
2004    }
2005    writeln!(out, "import com.fasterxml.jackson.annotation.JsonSubTypes;").ok();
2006    writeln!(out, "import com.fasterxml.jackson.annotation.JsonTypeInfo;").ok();
2007
2008    // Check if any field types need list/map/optional imports (only when not conflicting)
2009    let needs_list = !variant_names.contains("List")
2010        && enum_def
2011            .variants
2012            .iter()
2013            .any(|v| v.fields.iter().any(|f| matches!(&f.ty, TypeRef::Vec(_))));
2014    let needs_map = !variant_names.contains("Map")
2015        && enum_def
2016            .variants
2017            .iter()
2018            .any(|v| v.fields.iter().any(|f| matches!(&f.ty, TypeRef::Map(_, _))));
2019    let needs_optional =
2020        !variant_names.contains("Optional") && enum_def.variants.iter().any(|v| v.fields.iter().any(|f| f.optional));
2021    // Newtype/tuple variants (field name is a numeric index like "0") are flattened
2022    // into the parent JSON object using @JsonUnwrapped.
2023    let needs_unwrapped = enum_def
2024        .variants
2025        .iter()
2026        .any(|v| v.fields.len() == 1 && is_tuple_field_name(&v.fields[0].name));
2027    if needs_list {
2028        writeln!(out, "import java.util.List;").ok();
2029    }
2030    if needs_map {
2031        writeln!(out, "import java.util.Map;").ok();
2032    }
2033    if needs_optional {
2034        writeln!(out, "import java.util.Optional;").ok();
2035    }
2036    if needs_unwrapped {
2037        writeln!(out, "import com.fasterxml.jackson.annotation.JsonUnwrapped;").ok();
2038    }
2039    writeln!(out).ok();
2040
2041    emit_javadoc(&mut out, &enum_def.doc, "");
2042    // @JsonTypeInfo and @JsonSubTypes annotations
2043    writeln!(
2044        out,
2045        "@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = \"{tag_field}\", visible = false)"
2046    )
2047    .ok();
2048    writeln!(out, "@JsonSubTypes({{").ok();
2049    for (i, variant) in enum_def.variants.iter().enumerate() {
2050        let discriminator = variant
2051            .serde_rename
2052            .clone()
2053            .unwrap_or_else(|| java_apply_rename_all(&variant.name, enum_def.serde_rename_all.as_deref()));
2054        let comma = if i < enum_def.variants.len() - 1 { "," } else { "" };
2055        writeln!(
2056            out,
2057            "    @JsonSubTypes.Type(value = {}.{}.class, name = \"{}\"){}",
2058            enum_def.name, variant.name, discriminator, comma
2059        )
2060        .ok();
2061    }
2062    writeln!(out, "}})").ok();
2063    writeln!(out, "public sealed interface {} {{", enum_def.name).ok();
2064
2065    // Nested records for each variant
2066    for variant in &enum_def.variants {
2067        writeln!(out).ok();
2068        if variant.fields.is_empty() {
2069            // Unit variant
2070            if !variant.doc.is_empty() {
2071                let doc_summary = escape_javadoc_line(variant.doc.lines().next().unwrap_or("").trim());
2072                writeln!(out, "    /** {doc_summary} */").ok();
2073            }
2074            writeln!(out, "    record {}() implements {} {{", variant.name, enum_def.name).ok();
2075            writeln!(out, "    }}").ok();
2076        } else {
2077            // Build field list using fully qualified names where variant names shadow imports
2078            let field_parts: Vec<String> = variant
2079                .fields
2080                .iter()
2081                .map(|f| {
2082                    let ftype = if f.optional {
2083                        let inner = java_boxed_type(&f.ty);
2084                        let inner_str = inner.as_ref();
2085                        // Replace "List"/"Map" with fully qualified if conflicting
2086                        let inner_qualified = if inner_str.starts_with("List<") && variant_names.contains("List") {
2087                            inner_str.replacen("List<", "java.util.List<", 1)
2088                        } else if inner_str.starts_with("Map<") && variant_names.contains("Map") {
2089                            inner_str.replacen("Map<", "java.util.Map<", 1)
2090                        } else {
2091                            inner_str.to_string()
2092                        };
2093                        format!("{optional_type}<{inner_qualified}>")
2094                    } else {
2095                        let t = java_type(&f.ty);
2096                        let t_str = t.as_ref();
2097                        if t_str.starts_with("List<") && variant_names.contains("List") {
2098                            t_str.replacen("List<", "java.util.List<", 1)
2099                        } else if t_str.starts_with("Map<") && variant_names.contains("Map") {
2100                            t_str.replacen("Map<", "java.util.Map<", 1)
2101                        } else {
2102                            t_str.to_string()
2103                        }
2104                    };
2105                    // Tuple/newtype variants have numeric field names (e.g. "0", "_0").
2106                    // These are not real JSON keys — serde flattens the inner type's fields
2107                    // alongside the tag. Use @JsonUnwrapped so Jackson does the same.
2108                    if is_tuple_field_name(&f.name) {
2109                        format!("@JsonUnwrapped {ftype} value")
2110                    } else {
2111                        let json_name = f.name.trim_start_matches('_');
2112                        let jname = safe_java_field_name(json_name);
2113                        format!("@JsonProperty(\"{json_name}\") {ftype} {jname}")
2114                    }
2115                })
2116                .collect();
2117
2118            let single = format!(
2119                "    record {}({}) implements {} {{ }}",
2120                variant.name,
2121                field_parts.join(", "),
2122                enum_def.name
2123            );
2124
2125            if !variant.doc.is_empty() {
2126                let doc_summary = escape_javadoc_line(variant.doc.lines().next().unwrap_or("").trim());
2127                writeln!(out, "    /** {doc_summary} */").ok();
2128            }
2129            if single.len() > RECORD_LINE_WRAP_THRESHOLD && field_parts.len() > 1 {
2130                writeln!(out, "    record {}(", variant.name).ok();
2131                for (i, fp) in field_parts.iter().enumerate() {
2132                    let comma = if i < field_parts.len() - 1 { "," } else { "" };
2133                    writeln!(out, "        {}{}", fp, comma).ok();
2134                }
2135                writeln!(out, "    ) implements {} {{", enum_def.name).ok();
2136                writeln!(out, "    }}").ok();
2137            } else {
2138                writeln!(
2139                    out,
2140                    "    record {}({}) implements {} {{ }}",
2141                    variant.name,
2142                    field_parts.join(", "),
2143                    enum_def.name
2144                )
2145                .ok();
2146            }
2147        }
2148    }
2149
2150    writeln!(out).ok();
2151    writeln!(out, "}}").ok();
2152    out
2153}
2154
2155// ---------------------------------------------------------------------------
2156// Helper functions for FFI marshalling
2157// ---------------------------------------------------------------------------
2158
2159fn gen_ffi_layout(ty: &TypeRef) -> String {
2160    match ty {
2161        TypeRef::Primitive(prim) => java_ffi_type(prim).to_string(),
2162        TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => "ValueLayout.ADDRESS".to_string(),
2163        TypeRef::Bytes => "ValueLayout.ADDRESS".to_string(),
2164        TypeRef::Optional(inner) => gen_ffi_layout(inner),
2165        TypeRef::Vec(_) => "ValueLayout.ADDRESS".to_string(),
2166        TypeRef::Map(_, _) => "ValueLayout.ADDRESS".to_string(),
2167        TypeRef::Named(_) => "ValueLayout.ADDRESS".to_string(),
2168        TypeRef::Unit => "".to_string(),
2169        TypeRef::Duration => "ValueLayout.JAVA_LONG".to_string(),
2170    }
2171}
2172
2173fn marshal_param_to_ffi(out: &mut String, name: &str, ty: &TypeRef, opaque_types: &AHashSet<String>, prefix: &str) {
2174    match ty {
2175        TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => {
2176            let cname = "c".to_string() + name;
2177            writeln!(out, "            var {} = arena.allocateFrom({});", cname, name).ok();
2178        }
2179        TypeRef::Named(type_name) => {
2180            let cname = "c".to_string() + name;
2181            if opaque_types.contains(type_name.as_str()) {
2182                // Opaque handles: pass the inner MemorySegment via .handle()
2183                writeln!(out, "            var {} = {}.handle();", cname, name).ok();
2184            } else {
2185                // Non-opaque named types: serialize to JSON, call _from_json to get FFI pointer.
2186                // The pointer must be freed after the FFI call with _free.
2187                let type_snake = type_name.to_snake_case();
2188                let from_json_handle = format!(
2189                    "NativeLib.{}_{}_FROM_JSON",
2190                    prefix.to_uppercase(),
2191                    type_snake.to_uppercase()
2192                );
2193                let _free_handle = format!("NativeLib.{}_{}_FREE", prefix.to_uppercase(), type_snake.to_uppercase());
2194                writeln!(
2195                    out,
2196                    "            var {}Json = {} != null ? createObjectMapper().writeValueAsString({}) : null;",
2197                    cname, name, name
2198                )
2199                .ok();
2200                writeln!(
2201                    out,
2202                    "            var {}JsonSeg = {}Json != null ? arena.allocateFrom({}Json) : MemorySegment.NULL;",
2203                    cname, cname, cname
2204                )
2205                .ok();
2206                writeln!(out, "            var {} = {}Json != null", cname, cname).ok();
2207                writeln!(
2208                    out,
2209                    "                ? (MemorySegment) {}.invoke({}JsonSeg)",
2210                    from_json_handle, cname
2211                )
2212                .ok();
2213                writeln!(out, "                : MemorySegment.NULL;").ok();
2214            }
2215        }
2216        TypeRef::Optional(inner) => {
2217            // For optional types, marshal the inner type if not null
2218            match inner.as_ref() {
2219                TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => {
2220                    let cname = "c".to_string() + name;
2221                    writeln!(
2222                        out,
2223                        "            var {} = {} != null ? arena.allocateFrom({}) : MemorySegment.NULL;",
2224                        cname, name, name
2225                    )
2226                    .ok();
2227                }
2228                TypeRef::Named(type_name) => {
2229                    let cname = "c".to_string() + name;
2230                    if opaque_types.contains(type_name.as_str()) {
2231                        writeln!(
2232                            out,
2233                            "            var {} = {} != null ? {}.handle() : MemorySegment.NULL;",
2234                            cname, name, name
2235                        )
2236                        .ok();
2237                    } else {
2238                        // Non-opaque named type in Optional: serialize to JSON and call _from_json
2239                        let type_snake = type_name.to_snake_case();
2240                        let from_json_handle = format!(
2241                            "NativeLib.{}_{}_FROM_JSON",
2242                            prefix.to_uppercase(),
2243                            type_snake.to_uppercase()
2244                        );
2245                        writeln!(
2246                            out,
2247                            "            var {}Json = {} != null ? createObjectMapper().writeValueAsString({}) : null;",
2248                            cname, name, name
2249                        )
2250                        .ok();
2251                        writeln!(out, "            var {}JsonSeg = {}Json != null ? arena.allocateFrom({}Json) : MemorySegment.NULL;", cname, cname, cname).ok();
2252                        writeln!(out, "            var {} = {}Json != null", cname, cname).ok();
2253                        writeln!(
2254                            out,
2255                            "                ? (MemorySegment) {}.invoke({}JsonSeg)",
2256                            from_json_handle, cname
2257                        )
2258                        .ok();
2259                        writeln!(out, "                : MemorySegment.NULL;").ok();
2260                    }
2261                }
2262                _ => {
2263                    // Other optional types (primitives) pass through
2264                }
2265            }
2266        }
2267        TypeRef::Vec(_) | TypeRef::Map(_, _) => {
2268            // Vec/Map types: serialize to JSON string, then pass as a C string via arena.
2269            let cname = "c".to_string() + name;
2270            writeln!(
2271                out,
2272                "            var {}Json = createObjectMapper().writeValueAsString({});",
2273                cname, name
2274            )
2275            .ok();
2276            writeln!(out, "            var {} = arena.allocateFrom({}Json);", cname, cname).ok();
2277        }
2278        _ => {
2279            // Primitives and others pass through directly
2280        }
2281    }
2282}
2283
2284fn ffi_param_name(name: &str, ty: &TypeRef, _opaque_types: &AHashSet<String>) -> String {
2285    match ty {
2286        TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => "c".to_string() + name,
2287        TypeRef::Named(_) => "c".to_string() + name,
2288        TypeRef::Vec(_) | TypeRef::Map(_, _) => "c".to_string() + name,
2289        TypeRef::Optional(inner) => match inner.as_ref() {
2290            TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json | TypeRef::Named(_) => {
2291                "c".to_string() + name
2292            }
2293            _ => name.to_string(),
2294        },
2295        _ => name.to_string(),
2296    }
2297}
2298
2299/// Build a `FunctionDescriptor` string for a given return layout and parameter layouts.
2300/// Handles void returns (ofVoid) and non-void returns (of) correctly.
2301fn gen_function_descriptor(return_layout: &str, param_layouts: &[String]) -> String {
2302    if return_layout.is_empty() {
2303        // Void return
2304        if param_layouts.is_empty() {
2305            "FunctionDescriptor.ofVoid()".to_string()
2306        } else {
2307            format!("FunctionDescriptor.ofVoid({})", param_layouts.join(", "))
2308        }
2309    } else {
2310        // Non-void return
2311        if param_layouts.is_empty() {
2312            format!("FunctionDescriptor.of({})", return_layout)
2313        } else {
2314            format!("FunctionDescriptor.of({}, {})", return_layout, param_layouts.join(", "))
2315        }
2316    }
2317}
2318
2319/// Returns true if the given return type maps to an FFI ADDRESS that represents a string
2320/// (i.e. the FFI returns `*mut c_char` which must be unmarshaled and freed).
2321fn is_ffi_string_return(ty: &TypeRef) -> bool {
2322    match ty {
2323        TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => true,
2324        TypeRef::Optional(inner) => is_ffi_string_return(inner),
2325        _ => false,
2326    }
2327}
2328
2329/// Returns the appropriate Java cast type for non-string FFI return values.
2330fn java_ffi_return_cast(ty: &TypeRef) -> &'static str {
2331    match ty {
2332        TypeRef::Primitive(prim) => match prim {
2333            PrimitiveType::Bool => "boolean",
2334            PrimitiveType::U8 | PrimitiveType::I8 => "byte",
2335            PrimitiveType::U16 | PrimitiveType::I16 => "short",
2336            PrimitiveType::U32 | PrimitiveType::I32 => "int",
2337            PrimitiveType::U64 | PrimitiveType::I64 | PrimitiveType::Usize | PrimitiveType::Isize => "long",
2338            PrimitiveType::F32 => "float",
2339            PrimitiveType::F64 => "double",
2340        },
2341        TypeRef::Bytes | TypeRef::Vec(_) | TypeRef::Map(_, _) | TypeRef::Named(_) => "MemorySegment",
2342        _ => "MemorySegment",
2343    }
2344}
2345
2346fn gen_helper_methods(out: &mut String, prefix: &str, class_name: &str) {
2347    // Only emit helper methods that are actually called in the generated body.
2348    let needs_check_last_error = out.contains("checkLastError()");
2349    let needs_read_cstring = out.contains("readCString(");
2350    let needs_read_bytes = out.contains("readBytes(");
2351    let needs_create_object_mapper = out.contains("createObjectMapper()");
2352
2353    if !needs_check_last_error && !needs_read_cstring && !needs_read_bytes && !needs_create_object_mapper {
2354        return;
2355    }
2356
2357    writeln!(out, "    // Helper methods for FFI marshalling").ok();
2358    writeln!(out).ok();
2359
2360    if needs_check_last_error {
2361        // Reads the last FFI error code and, if non-zero, reads the error message and throws.
2362        // Called immediately after a null-pointer return from an FFI call.
2363        writeln!(out, "    private static void checkLastError() throws Throwable {{").ok();
2364        writeln!(
2365            out,
2366            "        int errCode = (int) NativeLib.{}_LAST_ERROR_CODE.invoke();",
2367            prefix.to_uppercase()
2368        )
2369        .ok();
2370        writeln!(out, "        if (errCode != 0) {{").ok();
2371        writeln!(
2372            out,
2373            "            var ctxPtr = (MemorySegment) NativeLib.{}_LAST_ERROR_CONTEXT.invoke();",
2374            prefix.to_uppercase()
2375        )
2376        .ok();
2377        writeln!(
2378            out,
2379            "            String msg = ctxPtr.reinterpret(Long.MAX_VALUE).getString(0);"
2380        )
2381        .ok();
2382        writeln!(out, "            throw new {}Exception(errCode, msg);", class_name).ok();
2383        writeln!(out, "        }}").ok();
2384        writeln!(out, "    }}").ok();
2385        writeln!(out).ok();
2386    }
2387
2388    if needs_create_object_mapper {
2389        // Emit a configured ObjectMapper factory:
2390        //   - findAndRegisterModules() to pick up jackson-datatype-jdk8 (Optional support)
2391        //   - ACCEPT_CASE_INSENSITIVE_ENUMS so enum names like "json_ld" match JsonLd, etc.
2392        // Field name mapping relies on explicit @JsonProperty annotations on record components
2393        // (generated by alef for snake_case FFI fields on camelCase Java records).
2394        writeln!(
2395            out,
2396            "    private static com.fasterxml.jackson.databind.ObjectMapper createObjectMapper() {{"
2397        )
2398        .ok();
2399        writeln!(out, "        return new com.fasterxml.jackson.databind.ObjectMapper()").ok();
2400        writeln!(
2401            out,
2402            "            .registerModule(new com.fasterxml.jackson.datatype.jdk8.Jdk8Module())"
2403        )
2404        .ok();
2405        writeln!(out, "            .findAndRegisterModules()").ok();
2406        writeln!(
2407            out,
2408            "            .setSerializationInclusion(com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL)"
2409        )
2410        .ok();
2411        writeln!(
2412            out,
2413            "            .configure(com.fasterxml.jackson.databind.MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS, true);"
2414        )
2415        .ok();
2416        writeln!(out, "    }}").ok();
2417        writeln!(out).ok();
2418    }
2419
2420    if needs_read_cstring {
2421        writeln!(out, "    private static String readCString(MemorySegment ptr) {{").ok();
2422        writeln!(out, "        if (ptr == null || ptr.address() == 0) {{").ok();
2423        writeln!(out, "            return null;").ok();
2424        writeln!(out, "        }}").ok();
2425        writeln!(out, "        return ptr.getUtf8String(0);").ok();
2426        writeln!(out, "    }}").ok();
2427        writeln!(out).ok();
2428    }
2429
2430    if needs_read_bytes {
2431        writeln!(
2432            out,
2433            "    private static byte[] readBytes(MemorySegment ptr, long len) {{"
2434        )
2435        .ok();
2436        writeln!(out, "        if (ptr == null || ptr.address() == 0) {{").ok();
2437        writeln!(out, "            return new byte[0];").ok();
2438        writeln!(out, "        }}").ok();
2439        writeln!(out, "        byte[] bytes = new byte[(int) len];").ok();
2440        writeln!(
2441            out,
2442            "        MemorySegment.copy(ptr, ValueLayout.JAVA_BYTE.byteSize() * 0, bytes, 0, (int) len);"
2443        )
2444        .ok();
2445        writeln!(out, "        return bytes;").ok();
2446        writeln!(out, "    }}").ok();
2447    }
2448}
2449
2450// ---------------------------------------------------------------------------
2451// Builder class for types with defaults
2452// ---------------------------------------------------------------------------
2453
2454/// Format a default value for an Optional field, wrapping it in Optional.of()
2455/// with proper Java literal syntax.
2456fn format_optional_value(ty: &TypeRef, default: &str) -> String {
2457    // Check if the default is already wrapped (e.g., "Optional.of(...)" or "Optional.empty()")
2458    if default.contains("Optional.") {
2459        return default.to_string();
2460    }
2461
2462    // Unwrap Optional types to get the inner type
2463    let inner_ty = match ty {
2464        TypeRef::Optional(inner) => inner.as_ref(),
2465        other => other,
2466    };
2467
2468    // Determine the proper literal suffix based on type
2469    let formatted_value = match inner_ty {
2470        TypeRef::Primitive(p) => match p {
2471            PrimitiveType::I64 | PrimitiveType::U64 | PrimitiveType::Isize | PrimitiveType::Usize => {
2472                // Add 'L' suffix for long values if not already present
2473                if default.ends_with('L') || default.ends_with('l') {
2474                    default.to_string()
2475                } else if default.parse::<i64>().is_ok() {
2476                    format!("{}L", default)
2477                } else {
2478                    default.to_string()
2479                }
2480            }
2481            PrimitiveType::F32 => {
2482                // Add 'f' suffix for float values if not already present
2483                if default.ends_with('f') || default.ends_with('F') {
2484                    default.to_string()
2485                } else if default.parse::<f32>().is_ok() {
2486                    format!("{}f", default)
2487                } else {
2488                    default.to_string()
2489                }
2490            }
2491            PrimitiveType::F64 => {
2492                // Double defaults can have optional 'd' suffix, but 0.0 is fine
2493                default.to_string()
2494            }
2495            _ => default.to_string(),
2496        },
2497        _ => default.to_string(),
2498    };
2499
2500    format!("Optional.of({})", formatted_value)
2501}
2502
2503fn gen_builder_class(package: &str, typ: &TypeDef) -> String {
2504    let mut body = String::with_capacity(2048);
2505
2506    emit_javadoc(&mut body, &typ.doc, "");
2507    writeln!(body, "public class {}Builder {{", typ.name).ok();
2508    writeln!(body).ok();
2509
2510    // Generate field declarations with defaults
2511    for field in &typ.fields {
2512        let field_name = safe_java_field_name(&field.name);
2513
2514        // Skip unnamed tuple fields (name is "_0", "_1", "0", "1", etc.) — Java requires named fields
2515        if field.name.starts_with('_') && field.name[1..].chars().all(|c| c.is_ascii_digit())
2516            || field.name.chars().next().is_none_or(|c| c.is_ascii_digit())
2517        {
2518            continue;
2519        }
2520
2521        // Duration maps to primitive `long` in the public record, but in builder
2522        // classes we use boxed `Long` so that `null` can represent "not set".
2523        let field_type = if field.optional {
2524            format!("Optional<{}>", java_boxed_type(&field.ty))
2525        } else if matches!(field.ty, TypeRef::Duration) {
2526            java_boxed_type(&field.ty).to_string()
2527        } else {
2528            java_type(&field.ty).to_string()
2529        };
2530
2531        let default_value = if field.optional {
2532            // For Optional fields, always use Optional.empty() or Optional.of(value)
2533            if let Some(default) = &field.default {
2534                // If there's an explicit default, wrap it in Optional.of()
2535                format_optional_value(&field.ty, default)
2536            } else {
2537                // If no default, use Optional.empty()
2538                "Optional.empty()".to_string()
2539            }
2540        } else {
2541            // For non-Optional fields, use regular defaults
2542            if let Some(default) = &field.default {
2543                default.clone()
2544            } else {
2545                match &field.ty {
2546                    TypeRef::String | TypeRef::Char | TypeRef::Path => "\"\"".to_string(),
2547                    TypeRef::Json => "null".to_string(),
2548                    TypeRef::Bytes => "new byte[0]".to_string(),
2549                    TypeRef::Primitive(p) => match p {
2550                        PrimitiveType::Bool => "false".to_string(),
2551                        PrimitiveType::F32 | PrimitiveType::F64 => "0.0".to_string(),
2552                        _ => "0".to_string(),
2553                    },
2554                    TypeRef::Vec(_) => "List.of()".to_string(),
2555                    TypeRef::Map(_, _) => "Map.of()".to_string(),
2556                    TypeRef::Optional(_) => "Optional.empty()".to_string(),
2557                    TypeRef::Duration => "null".to_string(),
2558                    _ => "null".to_string(),
2559                }
2560            }
2561        };
2562
2563        writeln!(body, "    private {} {} = {};", field_type, field_name, default_value).ok();
2564    }
2565
2566    writeln!(body).ok();
2567
2568    // Generate withXxx() methods
2569    for field in &typ.fields {
2570        // Skip unnamed tuple fields (name is "_0", "_1", "0", "1", etc.) — Java requires named fields
2571        if field.name.starts_with('_') && field.name[1..].chars().all(|c| c.is_ascii_digit())
2572            || field.name.chars().next().is_none_or(|c| c.is_ascii_digit())
2573        {
2574            continue;
2575        }
2576
2577        let field_name = safe_java_field_name(&field.name);
2578        let field_name_pascal = to_class_name(&field.name);
2579        let field_type = if field.optional {
2580            format!("Optional<{}>", java_boxed_type(&field.ty))
2581        } else if matches!(field.ty, TypeRef::Duration) {
2582            java_boxed_type(&field.ty).to_string()
2583        } else {
2584            java_type(&field.ty).to_string()
2585        };
2586
2587        writeln!(
2588            body,
2589            "    public {}Builder with{}({} value) {{",
2590            typ.name, field_name_pascal, field_type
2591        )
2592        .ok();
2593        writeln!(body, "        this.{} = value;", field_name).ok();
2594        writeln!(body, "        return this;").ok();
2595        writeln!(body, "    }}").ok();
2596        writeln!(body).ok();
2597    }
2598
2599    // Generate build() method
2600    writeln!(body, "    public {} build() {{", typ.name).ok();
2601    writeln!(body, "        return new {}(", typ.name).ok();
2602    let non_tuple_fields: Vec<_> = typ
2603        .fields
2604        .iter()
2605        .filter(|f| {
2606            // Include named fields (skip unnamed tuple fields)
2607            !(f.name.starts_with('_') && f.name[1..].chars().all(|c| c.is_ascii_digit())
2608                || f.name.chars().next().is_none_or(|c| c.is_ascii_digit()))
2609        })
2610        .collect();
2611    for (i, field) in non_tuple_fields.iter().enumerate() {
2612        let field_name = safe_java_field_name(&field.name);
2613        let comma = if i < non_tuple_fields.len() - 1 { "," } else { "" };
2614        writeln!(body, "            {}{}", field_name, comma).ok();
2615    }
2616    writeln!(body, "        );").ok();
2617    writeln!(body, "    }}").ok();
2618
2619    writeln!(body, "}}").ok();
2620
2621    // Now assemble with conditional imports based on what's actually used in the body
2622    let mut out = String::with_capacity(body.len() + 512);
2623
2624    writeln!(out, "// DO NOT EDIT - auto-generated by alef").ok();
2625    writeln!(out, "package {};", package).ok();
2626    writeln!(out).ok();
2627
2628    if body.contains("List<") {
2629        writeln!(out, "import java.util.List;").ok();
2630    }
2631    if body.contains("Map<") {
2632        writeln!(out, "import java.util.Map;").ok();
2633    }
2634    if body.contains("Optional<") {
2635        writeln!(out, "import java.util.Optional;").ok();
2636    }
2637
2638    writeln!(out).ok();
2639    out.push_str(&body);
2640
2641    out
2642}