//! M3 caixa renderer — emits adoption-surface artifacts from a
//! (defcaixa ...) source.
//!
//! Currently supports :ecosystem :rust-single-crate. Other
//! ecosystems land as follow-up commits per the example-caixas/
//! reference set. The M3 milestone (ACTION-AS-CAIXA roadmap)
//! widens this to the full 6-ecosystem set.

use anyhow::{bail, Result};
use std::collections::BTreeMap;
use std::fmt::Write as _;
use std::fs;
use std::path::{Path, PathBuf};

/// Render a parsed `.caixa.lisp` source into the target directory.
/// Returns the list of files written.
pub fn render(src: &str, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
    let parsed = parse(src)?;
    match parsed.ecosystem.as_str() {
        "rust-single-crate" => render_rust_single(&parsed, out, force),
        "rust-workspace" => render_rust_workspace(&parsed, out, force),
        "npm" => render_npm(&parsed, out, force),
        "python" => render_python(&parsed, out, force),
        "helm" => render_helm(&parsed, out, force),
        "github-action" => render_github_action(&parsed, out, force),
        // M3.3 — declarative manifests
        "go" => render_go(&parsed, out, force),
        "crystal" => render_crystal(&parsed, out, force),
        "dart" => render_dart(&parsed, out, force),
        "composer" => render_composer(&parsed, out, force),
        "julia" => render_julia(&parsed, out, force),
        // M3.4 — XML + s-expr
        "java-maven" => render_maven(&parsed, out, force),
        "dotnet-csproj" => render_csproj(&parsed, out, force),
        "ocaml-dune" => render_dune(&parsed, out, force),
        // M3.5 — code-as-manifest
        "java-gradle-kts" => render_gradle_kts(&parsed, out, force),
        "swift-spm" => render_swift_spm(&parsed, out, force),
        "elixir-mix" => render_elixir_mix(&parsed, out, force),
        "ruby-gem" => render_ruby_gem(&parsed, out, force),
        "zig" => render_zig(&parsed, out, force),
        "nim-nimble" => render_nim_nimble(&parsed, out, force),
        // M3.6 — niche / popular declarative + code manifests
        "scala-sbt" => render_sbt(&parsed, out, force),
        "clojure-deps" => render_clj_deps(&parsed, out, force),
        "r-description" => render_r(&parsed, out, force),
        "lua-rockspec" => render_rockspec(&parsed, out, force),
        "cpp-conan" => render_conan(&parsed, out, force),
        // M3.7 — multi-PM / build-system extras
        "python-conda" => render_conda(&parsed, out, force),
        "python-pipenv" => render_pipenv(&parsed, out, force),
        "python-pdm" => render_pdm(&parsed, out, force),
        "js-deno" => render_deno(&parsed, out, force),
        "js-pnpm" => render_pnpm(&parsed, out, force),
        "cpp-vcpkg" => render_vcpkg(&parsed, out, force),
        "cpp-meson" => render_meson(&parsed, out, force),
        "fortran-fpm" => render_fpm(&parsed, out, force),
        "gleam" => render_gleam(&parsed, out, force),
        "ada-alire" => render_alire(&parsed, out, force),
        "haskell-cabal" => render_cabal(&parsed, out, force),
        "racket-info" => render_racket(&parsed, out, force),
        other => bail!(
            "ecosystem '{other}' not supported. 33 supported: \
             rust-single-crate, rust-workspace, npm, python, helm, \
             github-action, go, crystal, dart, composer, julia, \
             java-maven, dotnet-csproj, ocaml-dune, java-gradle-kts, \
             swift-spm, elixir-mix, ruby-gem, zig, nim-nimble, \
             scala-sbt, clojure-deps, r-description, lua-rockspec, cpp-conan, \
             python-conda, python-pipenv, python-pdm, js-deno, js-pnpm, \
             cpp-vcpkg, cpp-meson, fortran-fpm, gleam, ada-alire, \
             haskell-cabal, racket-info."
        ),
    }
}

fn render_go(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
    let mut written = vec![];
    fs::create_dir_all(out)?;
    let gomod = out.join("go.mod");
    if !gomod.exists() || force {
        let pkg = &c.package;
        let mut s = String::new();
        let name = pkg.get("name").cloned().unwrap_or_else(|| c.name.clone());
        let module_path = pkg.get("repository").cloned()
            .map(|r| r.trim_start_matches("https://").trim_start_matches("http://").to_string())
            .unwrap_or_else(|| format!("github.com/pleme-io/{name}"));
        s.push_str(&format!("module {module_path}\n\n"));
        let go_ver = c.supports_raw.as_deref()
            .and_then(|r| read_keyword_in_block(r, ":go"))
            .map(|v| v.trim_matches('"').trim_start_matches(">=").to_string())
            .unwrap_or_else(|| "1.22".to_string());
        s.push_str(&format!("go {go_ver}\n"));
        let deps = parse_deps_raw(c.dependencies_raw.as_deref().unwrap_or(""));
        if !deps.runtime.is_empty() {
            s.push_str("\nrequire (\n");
            for d in &deps.runtime {
                if let (Some(n), Some(v)) = (d.get("name"), d.get("version")) {
                    s.push_str(&format!("\t{n} {v}\n"));
                }
            }
            s.push_str(")\n");
        }
        fs::write(&gomod, s)?;
        written.push(gomod);
    }
    render_ci_shims(c, out, force, &mut written)?;
    Ok(written)
}

fn render_crystal(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
    use crate::yaml_ast::{render as render_yaml, Value as YVal};
    let mut written = vec![];
    fs::create_dir_all(out)?;
    let shard = out.join("shard.yml");
    if !shard.exists() || force {
        let pkg = &c.package;
        let mut m = YVal::map();
        if let Some(n) = pkg.get("name") { m.insert("name", YVal::s(n)); }
        if let Some(v) = pkg.get("version") { m.insert("version", YVal::s(v)); }
        if let Some(l) = pkg.get("license") { m.insert("license", YVal::s(l)); }
        if let Some(authors) = pkg.get("authors") {
            m.insert("authors", YVal::arr([YVal::s(authors)]));
        }
        if let Some(supports) = c.supports_raw.as_deref() {
            if let Some(cr) = read_keyword_in_block(supports, ":crystal") {
                m.insert("crystal", YVal::s(cr.trim_matches('"')));
            }
        }
        fs::write(&shard, render_yaml(&m))?;
        written.push(shard);
    }
    render_ci_shims(c, out, force, &mut written)?;
    Ok(written)
}

fn render_dart(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
    use crate::yaml_ast::{render as render_yaml, Value as YVal};
    let mut written = vec![];
    fs::create_dir_all(out)?;
    let pubspec = out.join("pubspec.yaml");
    if !pubspec.exists() || force {
        let pkg = &c.package;
        let mut m = YVal::map();
        if let Some(n) = pkg.get("name") { m.insert("name", YVal::s(n)); }
        if let Some(v) = pkg.get("version") { m.insert("version", YVal::s(v)); }
        if let Some(d) = pkg.get("description") { m.insert("description", YVal::s(d)); }
        if let Some(r) = pkg.get("repository") { m.insert("repository", YVal::s(r)); }
        if let Some(supports) = c.supports_raw.as_deref() {
            if let Some(sdk) = read_keyword_in_block(supports, ":dart") {
                let mut env = YVal::map();
                env.insert("sdk", YVal::s(sdk.trim_matches('"')));
                m.insert("environment", env);
            }
        }
        let deps = parse_deps_raw(c.dependencies_raw.as_deref().unwrap_or(""));
        if !deps.runtime.is_empty() {
            let mut rt = YVal::map();
            for d in &deps.runtime {
                if let (Some(n), Some(v)) = (d.get("name"), d.get("version")) {
                    rt.insert(n, YVal::s(v));
                }
            }
            m.insert("dependencies", rt);
        }
        if !deps.dev.is_empty() {
            let mut dv = YVal::map();
            for d in &deps.dev {
                if let (Some(n), Some(v)) = (d.get("name"), d.get("version")) {
                    dv.insert(n, YVal::s(v));
                }
            }
            m.insert("dev_dependencies", dv);
        }
        fs::write(&pubspec, render_yaml(&m))?;
        written.push(pubspec);
    }
    render_ci_shims(c, out, force, &mut written)?;
    Ok(written)
}

fn render_composer(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
    use crate::json_ast::{render as render_json, Value as JVal};
    let mut written = vec![];
    fs::create_dir_all(out)?;
    let composer = out.join("composer.json");
    if !composer.exists() || force {
        let pkg = &c.package;
        let mut root = JVal::obj();
        if let Some(n) = pkg.get("name") { root.insert("name", JVal::s(n)); }
        if let Some(d) = pkg.get("description") { root.insert("description", JVal::s(d)); }
        root.insert("type", JVal::s("library"));
        if let Some(l) = pkg.get("license") { root.insert("license", JVal::s(l)); }

        let deps = parse_deps_raw(c.dependencies_raw.as_deref().unwrap_or(""));
        let php_req = c.supports_raw.as_deref()
            .and_then(|r| read_keyword_in_block(r, ":php"))
            .map(|v| v.trim_matches('"').to_string())
            .unwrap_or_else(|| ">=8.2".to_string());

        let mut require = JVal::obj();
        require.insert("php", JVal::s(php_req));
        for d in &deps.runtime {
            if let (Some(n), Some(v)) = (d.get("name"), d.get("version")) {
                require.insert(n, JVal::s(v));
            }
        }
        root.insert("require", require);

        if !deps.dev.is_empty() {
            let mut dev = JVal::obj();
            for d in &deps.dev {
                if let Some(n) = d.get("name") {
                    let v = d.get("version").cloned().unwrap_or_else(|| "*".to_string());
                    dev.insert(n, JVal::s(v));
                }
            }
            root.insert("require-dev", dev);
        }
        fs::write(&composer, render_json(&root))?;
        written.push(composer);
    }
    render_ci_shims(c, out, force, &mut written)?;
    Ok(written)
}

fn xml_escape(s: &str) -> String {
    s.replace('&', "&amp;").replace('<', "&lt;").replace('>', "&gt;")
}

fn render_maven(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
    use crate::xml_ast::{render_document, Element};
    let mut written = vec![];
    fs::create_dir_all(out)?;
    let pom = out.join("pom.xml");
    if !pom.exists() || force {
        let pkg = &c.package;
        let group = pkg.get("group-id").cloned().unwrap_or_else(|| "io.pleme".to_string());
        let artifact = pkg.get("name").cloned().unwrap_or_else(|| c.name.clone());
        let version = pkg.get("version").cloned().unwrap_or_else(|| "0.1.0".to_string());

        let mut project = Element::new("project")
            .attr("xmlns", "http://maven.apache.org/POM/4.0.0")
            .attr("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
            .attr("xsi:schemaLocation", "http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd")
            .child(Element::new("modelVersion").text("4.0.0"))
            .child(Element::new("groupId").text(group))
            .child(Element::new("artifactId").text(artifact))
            .child(Element::new("version").text(version))
            .child(Element::new("packaging").text("jar"));

        if let Some(d) = pkg.get("description") {
            project.push(Element::new("description").text(d));
        }
        if let Some(l) = pkg.get("license") {
            let lic = Element::new("license").child(Element::new("name").text(l));
            project.push(Element::new("licenses").child(lic));
        }
        if let Some(r) = pkg.get("repository") {
            project.push(Element::new("scm").child(Element::new("url").text(r)));
        }

        let deps = parse_deps_raw(c.dependencies_raw.as_deref().unwrap_or(""));
        if !deps.runtime.is_empty() || !deps.dev.is_empty() {
            let mut dependencies = Element::new("dependencies");
            for d in &deps.runtime {
                if let Some(n) = d.get("name") {
                    let v = d.get("version").cloned().unwrap_or_default();
                    let g = d.get("group-id").cloned().unwrap_or_else(|| "org.example".to_string());
                    let mut dep = Element::new("dependency")
                        .child(Element::new("groupId").text(g))
                        .child(Element::new("artifactId").text(n));
                    if !v.is_empty() {
                        dep.push(Element::new("version").text(v));
                    }
                    dependencies.push(dep);
                }
            }
            for d in &deps.dev {
                if let Some(n) = d.get("name") {
                    let v = d.get("version").cloned().unwrap_or_default();
                    let g = d.get("group-id").cloned().unwrap_or_else(|| "org.example".to_string());
                    let mut dep = Element::new("dependency")
                        .child(Element::new("groupId").text(g))
                        .child(Element::new("artifactId").text(n));
                    if !v.is_empty() {
                        dep.push(Element::new("version").text(v));
                    }
                    dep.push(Element::new("scope").text("test"));
                    dependencies.push(dep);
                }
            }
            project.push(dependencies);
        }
        fs::write(&pom, render_document(&project))?;
        written.push(pom);
    }
    render_ci_shims(c, out, force, &mut written)?;
    Ok(written)
}

fn render_csproj(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
    use crate::xml_ast::Element;
    let mut written = vec![];
    fs::create_dir_all(out)?;
    let pkg = &c.package;
    let name = pkg.get("name").cloned().unwrap_or_else(|| c.name.clone());
    let path = out.join(format!("{name}.csproj"));
    if !path.exists() || force {
        let target = c.supports_raw.as_deref()
            .and_then(|r| read_keyword_in_block(r, ":dotnet"))
            .map(|v| v.trim_matches('"').to_string())
            .unwrap_or_else(|| "net8.0".to_string());

        let output_type = if c.kind == "Binario" { "Exe" } else { "Library" };
        let mut props = Element::new("PropertyGroup")
            .child(Element::new("TargetFramework").text(target))
            .child(Element::new("OutputType").text(output_type));
        if let Some(v) = pkg.get("version") { props.push(Element::new("Version").text(v)); }
        if let Some(a) = pkg.get("authors") { props.push(Element::new("Authors").text(a)); }
        if let Some(d) = pkg.get("description") { props.push(Element::new("Description").text(d)); }
        if let Some(l) = pkg.get("license") { props.push(Element::new("PackageLicenseExpression").text(l)); }
        if let Some(r) = pkg.get("repository") { props.push(Element::new("RepositoryUrl").text(r)); }

        let mut project = Element::new("Project")
            .attr("Sdk", "Microsoft.NET.Sdk")
            .child(props);

        let deps = parse_deps_raw(c.dependencies_raw.as_deref().unwrap_or(""));
        if !deps.runtime.is_empty() || !deps.dev.is_empty() {
            let mut items = Element::new("ItemGroup");
            for d in &deps.runtime {
                if let Some(n) = d.get("name") {
                    let v = d.get("version").cloned().unwrap_or_default();
                    items.push(Element::new("PackageReference").attr("Include", n).attr("Version", v));
                }
            }
            for d in &deps.dev {
                if let Some(n) = d.get("name") {
                    let v = d.get("version").cloned().unwrap_or_default();
                    items.push(
                        Element::new("PackageReference")
                            .attr("Include", n).attr("Version", v).attr("PrivateAssets", "all"),
                    );
                }
            }
            project.push(items);
        }
        // .csproj has no XML declaration — render the element only.
        let mut out_str = String::new();
        crate::xml_ast::render_doc_no_decl(&project, &mut out_str);
        fs::write(&path, out_str)?;
        written.push(path);
    }
    render_ci_shims(c, out, force, &mut written)?;
    Ok(written)
}

fn render_dune(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
    use crate::sexp_ast::{render_forms, SExp};
    let mut written = vec![];
    fs::create_dir_all(out)?;
    let dune = out.join("dune-project");
    if !dune.exists() || force {
        let pkg = &c.package;
        let name = pkg.get("name").cloned().unwrap_or_else(|| c.name.clone());

        let mut forms: Vec<SExp> = vec![
            SExp::list([SExp::sym("lang"), SExp::sym("dune"), SExp::sym("3.14")]),
            SExp::list([SExp::sym("name"), SExp::sym(&name)]),
        ];
        if let Some(v) = pkg.get("version") {
            forms.push(SExp::list([SExp::sym("version"), SExp::sym(v)]));
        }
        forms.push(SExp::list([SExp::sym("generate_opam_files"), SExp::sym("true")]));
        if let Some(r) = pkg.get("repository") {
            if r.contains("github.com") {
                let gh = r.trim_start_matches("https://github.com/").trim_end_matches(".git");
                forms.push(SExp::list([
                    SExp::sym("source"),
                    SExp::list([SExp::sym("github"), SExp::sym(gh)]),
                ]));
            }
        }
        if let Some(l) = pkg.get("license") {
            forms.push(SExp::list([SExp::sym("license"), SExp::sym(l)]));
        }
        forms.push(SExp::list([SExp::sym("authors"), SExp::str("pleme-io")]));
        forms.push(SExp::list([SExp::sym("maintainers"), SExp::str("engineering@pleme.io")]));

        let ocaml_req = c.supports_raw.as_deref()
            .and_then(|r| read_keyword_in_block(r, ":ocaml"))
            .map(|v| v.trim_matches('"').trim_start_matches(">=").to_string())
            .unwrap_or_else(|| "4.14".to_string());
        let deps = parse_deps_raw(c.dependencies_raw.as_deref().unwrap_or(""));

        let mut depends: Vec<SExp> = vec![
            SExp::sym("depends"),
            SExp::list([
                SExp::sym("ocaml"),
                SExp::list([SExp::sym(">="), SExp::sym(&ocaml_req)]),
            ]),
            SExp::sym("dune"),
        ];
        for d in &deps.runtime {
            if let Some(n) = d.get("name") { depends.push(SExp::sym(n)); }
        }
        for d in &deps.dev {
            if let Some(n) = d.get("name") {
                depends.push(SExp::list([SExp::sym(n), SExp::kw("with-test")]));
            }
        }

        let mut pkg_form: Vec<SExp> = vec![
            SExp::sym("package"),
            SExp::list([SExp::sym("name"), SExp::sym(&name)]),
        ];
        if let Some(d) = pkg.get("description") {
            pkg_form.push(SExp::list([SExp::sym("synopsis"), SExp::str(d)]));
        }
        pkg_form.push(SExp::List(depends));
        forms.push(SExp::List(pkg_form));

        fs::write(&dune, render_forms(&forms))?;
        written.push(dune);
    }
    render_ci_shims(c, out, force, &mut written)?;
    Ok(written)
}

// ─── M3.5 — code-as-manifest emitters ──────────────────────────

fn render_gradle_kts(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
    let mut written = vec![];
    fs::create_dir_all(out)?;
    let pkg = &c.package;
    let name = pkg.get("name").cloned().unwrap_or_else(|| c.name.clone());
    let version = pkg.get("version").cloned().unwrap_or_else(|| "0.1.0".to_string());
    let group = pkg.get("group-id").cloned().unwrap_or_else(|| "io.pleme".to_string());
    let java_ver = c.supports_raw.as_deref()
        .and_then(|r| read_keyword_in_block(r, ":java"))
        .map(|v| v.trim_matches('"').trim_start_matches(">=").to_string())
        .unwrap_or_else(|| "17".to_string());

    // settings.gradle.kts
    let settings = out.join("settings.gradle.kts");
    if !settings.exists() || force {
        fs::write(&settings, format!("rootProject.name = \"{name}\"\n"))?;
        written.push(settings);
    }

    // build.gradle.kts
    let build = out.join("build.gradle.kts");
    if !build.exists() || force {
        let is_app = c.kind == "Binario";
        let plugin = if is_app { "application" } else { "`java-library`" };
        let mut s = format!("plugins {{ {plugin} }}\n\n");
        s.push_str(&format!("group = \"{group}\"\nversion = \"{version}\"\n\n"));
        s.push_str("repositories { mavenCentral() }\n\n");
        s.push_str("dependencies {\n");
        let deps = parse_deps_raw(c.dependencies_raw.as_deref().unwrap_or(""));
        for d in &deps.runtime {
            if let (Some(n), Some(v)) = (d.get("name"), d.get("version")) {
                let g = d.get("group-id").cloned().unwrap_or_else(|| "com.example".to_string());
                s.push_str(&format!("    implementation(\"{g}:{n}:{v}\")\n"));
            }
        }
        for d in &deps.dev {
            if let (Some(n), Some(v)) = (d.get("name"), d.get("version")) {
                let g = d.get("group-id").cloned().unwrap_or_else(|| "com.example".to_string());
                s.push_str(&format!("    testImplementation(\"{g}:{n}:{v}\")\n"));
            }
        }
        s.push_str("}\n\n");
        s.push_str(&format!("java {{ toolchain {{ languageVersion = JavaLanguageVersion.of({java_ver}) }} }}\n"));
        fs::write(&build, s)?;
        written.push(build);
    }
    render_ci_shims(c, out, force, &mut written)?;
    Ok(written)
}

fn render_swift_spm(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
    let mut written = vec![];
    fs::create_dir_all(out)?;
    let pkg = &c.package;
    let name = pkg.get("name").cloned().unwrap_or_else(|| c.name.clone());
    let pkg_swift = out.join("Package.swift");
    if !pkg_swift.exists() || force {
        let tools_ver = c.supports_raw.as_deref()
            .and_then(|r| read_keyword_in_block(r, ":swift-tools"))
            .map(|v| v.trim_matches('"').to_string())
            .unwrap_or_else(|| "5.9".to_string());
        let is_exe = c.kind == "Binario";
        let mut s = format!("// swift-tools-version:{tools_ver}\nimport PackageDescription\n\nlet package = Package(\n    name: \"{name}\",\n");
        s.push_str("    platforms: [.macOS(.v13)],\n");
        if is_exe {
            s.push_str(&format!("    products: [.executable(name: \"{name}\", targets: [\"{name}\"])],\n"));
        } else {
            s.push_str(&format!("    products: [.library(name: \"{name}\", targets: [\"{name}\"])],\n"));
        }
        // Dependencies
        let deps = parse_deps_raw(c.dependencies_raw.as_deref().unwrap_or(""));
        if !deps.runtime.is_empty() {
            s.push_str("    dependencies: [\n");
            for d in &deps.runtime {
                if let (Some(n), Some(v)) = (d.get("name"), d.get("version")) {
                    let url = d.get("repository").cloned()
                        .unwrap_or_else(|| format!("https://github.com/{n}/{n}"));
                    s.push_str(&format!("        .package(url: \"{url}\", from: \"{v}\"),\n"));
                }
            }
            s.push_str("    ],\n");
        }
        let target_kind = if is_exe { "executableTarget" } else { "target" };
        s.push_str(&format!("    targets: [\n        .{target_kind}(name: \"{name}\"),\n    ]\n)\n"));
        fs::write(&pkg_swift, s)?;
        written.push(pkg_swift);
    }
    render_ci_shims(c, out, force, &mut written)?;
    Ok(written)
}

fn render_elixir_mix(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
    let mut written = vec![];
    fs::create_dir_all(out)?;
    let pkg = &c.package;
    let name = pkg.get("name").cloned().unwrap_or_else(|| c.name.clone());
    let mix = out.join("mix.exs");
    if !mix.exists() || force {
        let module = format!("{}.MixProject", name.replace('-', "_")
            .split('_').map(|p| {
                let mut chars = p.chars();
                match chars.next() {
                    Some(c) => c.to_uppercase().chain(chars).collect(),
                    None => String::new(),
                }
            }).collect::<String>());
        let version = pkg.get("version").cloned().unwrap_or_else(|| "0.1.0".to_string());
        let elixir_req = c.supports_raw.as_deref()
            .and_then(|r| read_keyword_in_block(r, ":elixir"))
            .map(|v| v.trim_matches('"').to_string())
            .unwrap_or_else(|| "~> 1.16".to_string());
        let desc = pkg.get("description").cloned().unwrap_or_default();
        let license = pkg.get("license").cloned().unwrap_or_else(|| "MIT".to_string());
        let mut s = format!("defmodule {module} do\n  use Mix.Project\n\n  def project, do: [\n");
        s.push_str(&format!("    app: :{},\n", name.replace('-', "_")));
        s.push_str(&format!("    version: \"{version}\",\n"));
        s.push_str(&format!("    elixir: \"{elixir_req}\",\n"));
        s.push_str(&format!("    description: \"{desc}\",\n"));
        s.push_str(&format!("    package: [licenses: [\"{license}\"]],\n"));
        s.push_str("    deps: deps()\n  ]\n\n");
        s.push_str("  def application, do: [extra_applications: [:logger]]\n\n");
        s.push_str("  defp deps, do: [\n");
        let deps = parse_deps_raw(c.dependencies_raw.as_deref().unwrap_or(""));
        for d in &deps.runtime {
            if let (Some(n), Some(v)) = (d.get("name"), d.get("version")) {
                s.push_str(&format!("    {{:{n}, \"{v}\"}},\n"));
            }
        }
        for d in &deps.dev {
            if let (Some(n), Some(v)) = (d.get("name"), d.get("version")) {
                s.push_str(&format!("    {{:{n}, \"{v}\", only: :dev, runtime: false}},\n"));
            }
        }
        s.push_str("  ]\nend\n");
        fs::write(&mix, s)?;
        written.push(mix);
    }
    render_ci_shims(c, out, force, &mut written)?;
    Ok(written)
}

fn render_ruby_gem(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
    let mut written = vec![];
    fs::create_dir_all(out)?;
    let pkg = &c.package;
    let name = pkg.get("name").cloned().unwrap_or_else(|| c.name.clone());
    let gemspec = out.join(format!("{name}.gemspec"));
    if !gemspec.exists() || force {
        let version = pkg.get("version").cloned().unwrap_or_else(|| "0.1.0".to_string());
        let desc = pkg.get("description").cloned().unwrap_or_default();
        let license = pkg.get("license").cloned().unwrap_or_else(|| "MIT".to_string());
        let homepage = pkg.get("homepage").or_else(|| pkg.get("repository"))
            .cloned().unwrap_or_else(|| format!("https://github.com/pleme-io/{name}"));
        let ruby_req = c.supports_raw.as_deref()
            .and_then(|r| read_keyword_in_block(r, ":ruby"))
            .map(|v| v.trim_matches('"').to_string())
            .unwrap_or_else(|| ">= 3.0".to_string());
        let mut s = String::from("Gem::Specification.new do |spec|\n");
        s.push_str(&format!("  spec.name          = '{name}'\n"));
        s.push_str(&format!("  spec.version       = '{version}'\n"));
        s.push_str("  spec.authors       = ['pleme-io']\n");
        s.push_str("  spec.email         = ['engineering@pleme.io']\n");
        s.push_str(&format!("  spec.summary       = '{desc}'\n"));
        s.push_str(&format!("  spec.description   = '{desc}'\n"));
        s.push_str(&format!("  spec.license       = '{license}'\n"));
        s.push_str(&format!("  spec.homepage      = '{homepage}'\n"));
        s.push_str("  spec.files         = Dir['lib/**/*.rb']\n");
        s.push_str(&format!("  spec.required_ruby_version = '{ruby_req}'\n"));
        let deps = parse_deps_raw(c.dependencies_raw.as_deref().unwrap_or(""));
        for d in &deps.runtime {
            if let (Some(n), Some(v)) = (d.get("name"), d.get("version")) {
                s.push_str(&format!("  spec.add_dependency '{n}', '{v}'\n"));
            }
        }
        for d in &deps.dev {
            if let (Some(n), Some(v)) = (d.get("name"), d.get("version")) {
                s.push_str(&format!("  spec.add_development_dependency '{n}', '{v}'\n"));
            }
        }
        s.push_str("end\n");
        fs::write(&gemspec, s)?;
        written.push(gemspec);
    }
    render_ci_shims(c, out, force, &mut written)?;
    Ok(written)
}

fn render_zig(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
    let mut written = vec![];
    fs::create_dir_all(out)?;
    let pkg = &c.package;
    let name = pkg.get("name").cloned().unwrap_or_else(|| c.name.clone());
    let version = pkg.get("version").cloned().unwrap_or_else(|| "0.1.0".to_string());
    let min_zig = c.supports_raw.as_deref()
        .and_then(|r| read_keyword_in_block(r, ":zig"))
        .map(|v| v.trim_matches('"').to_string())
        .unwrap_or_else(|| "0.14.0".to_string());
    // build.zig.zon (manifest)
    let zon = out.join("build.zig.zon");
    if !zon.exists() || force {
        let mut s = format!(".{{\n    .name = .{},\n    .version = \"{version}\",\n    .minimum_zig_version = \"{min_zig}\",\n",
            name.replace('-', "_"));
        s.push_str("    .dependencies = .{},\n");
        s.push_str("    .paths = .{ \"build.zig\", \"build.zig.zon\", \"src\" },\n}\n");
        fs::write(&zon, s)?;
        written.push(zon);
    }
    // build.zig (imperative)
    let build = out.join("build.zig");
    if !build.exists() || force {
        let is_exe = c.kind == "Binario";
        let kind_fn = if is_exe { "addExecutable" } else { "addLibrary" };
        let mut s = String::from("const std = @import(\"std\");\n\npub fn build(b: *std.Build) void {\n");
        s.push_str("    const target = b.standardTargetOptions(.{});\n");
        s.push_str("    const optimize = b.standardOptimizeOption(.{});\n");
        s.push_str(&format!("    const artifact = b.{kind_fn}(.{{\n"));
        s.push_str(&format!("        .name = \"{name}\",\n"));
        s.push_str("        .root_source_file = b.path(\"src/main.zig\"),\n");
        s.push_str("        .target = target,\n        .optimize = optimize,\n    });\n");
        s.push_str("    b.installArtifact(artifact);\n}\n");
        fs::write(&build, s)?;
        written.push(build);
    }
    render_ci_shims(c, out, force, &mut written)?;
    Ok(written)
}

fn render_nim_nimble(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
    let mut written = vec![];
    fs::create_dir_all(out)?;
    let pkg = &c.package;
    let name = pkg.get("name").cloned().unwrap_or_else(|| c.name.clone());
    let nimble = out.join(format!("{name}.nimble"));
    if !nimble.exists() || force {
        let version = pkg.get("version").cloned().unwrap_or_else(|| "0.1.0".to_string());
        let desc = pkg.get("description").cloned().unwrap_or_default();
        let license = pkg.get("license").cloned().unwrap_or_else(|| "MIT".to_string());
        let nim_req = c.supports_raw.as_deref()
            .and_then(|r| read_keyword_in_block(r, ":nim"))
            .map(|v| v.trim_matches('"').trim_start_matches(">=").trim().to_string())
            .unwrap_or_else(|| "2.0.0".to_string());
        let mut s = format!("version       = \"{version}\"\n");
        s.push_str("author        = \"pleme-io\"\n");
        s.push_str(&format!("description   = \"{desc}\"\n"));
        s.push_str(&format!("license       = \"{license}\"\n"));
        s.push_str("srcDir        = \"src\"\n");
        if c.kind == "Binario" {
            s.push_str(&format!("bin           = @[\"{name}\"]\n"));
        }
        s.push_str(&format!("\nrequires \"nim >= {nim_req}\"\n"));
        let deps = parse_deps_raw(c.dependencies_raw.as_deref().unwrap_or(""));
        for d in &deps.runtime {
            if let (Some(n), Some(v)) = (d.get("name"), d.get("version")) {
                s.push_str(&format!("requires \"{n} >= {v}\"\n"));
            }
        }
        fs::write(&nimble, s)?;
        written.push(nimble);
    }
    render_ci_shims(c, out, force, &mut written)?;
    Ok(written)
}

// ─── M3.6 — niche / popular emitters ───────────────────────────

fn render_sbt(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
    let mut written = vec![];
    fs::create_dir_all(out)?;
    let pkg = &c.package;
    let build = out.join("build.sbt");
    if !build.exists() || force {
        let name = pkg.get("name").cloned().unwrap_or_else(|| c.name.clone());
        let version = pkg.get("version").cloned().unwrap_or_else(|| "0.1.0".to_string());
        let scala_ver = c.supports_raw.as_deref()
            .and_then(|r| read_keyword_in_block(r, ":scala"))
            .map(|v| v.trim_matches('"').to_string())
            .unwrap_or_else(|| "3.4.0".to_string());
        let mut s = format!("name := \"{name}\"\nversion := \"{version}\"\nscalaVersion := \"{scala_ver}\"\n");
        if let Some(d) = pkg.get("description") {
            s.push_str(&format!("description := \"{}\"\n", d.replace('"', "\\\"")));
        }
        let deps = parse_deps_raw(c.dependencies_raw.as_deref().unwrap_or(""));
        if !deps.runtime.is_empty() {
            s.push_str("\nlibraryDependencies ++= Seq(\n");
            for d in &deps.runtime {
                if let (Some(n), Some(v)) = (d.get("name"), d.get("version")) {
                    let g = d.get("group-id").cloned().unwrap_or_else(|| "org.example".to_string());
                    s.push_str(&format!("  \"{g}\" %% \"{n}\" % \"{v}\",\n"));
                }
            }
            s.push_str(")\n");
        }
        fs::write(&build, s)?;
        written.push(build);
    }
    render_ci_shims(c, out, force, &mut written)?;
    Ok(written)
}

fn render_clj_deps(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
    let mut written = vec![];
    fs::create_dir_all(out)?;
    let deps_edn = out.join("deps.edn");
    if !deps_edn.exists() || force {
        let _ = c.package.len();
        let mut s = String::from("{:paths [\"src\" \"resources\"]\n :deps {\n");
        let deps = parse_deps_raw(c.dependencies_raw.as_deref().unwrap_or(""));
        for d in &deps.runtime {
            if let (Some(n), Some(v)) = (d.get("name"), d.get("version")) {
                s.push_str(&format!("  {n} {{:mvn/version \"{v}\"}}\n"));
            }
        }
        s.push_str(" }}\n");
        fs::write(&deps_edn, s)?;
        written.push(deps_edn);
    }
    render_ci_shims(c, out, force, &mut written)?;
    Ok(written)
}

fn render_r(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
    let mut written = vec![];
    fs::create_dir_all(out)?;
    let desc = out.join("DESCRIPTION");
    if !desc.exists() || force {
        let pkg = &c.package;
        let name = pkg.get("name").cloned().unwrap_or_else(|| c.name.clone());
        let version = pkg.get("version").cloned().unwrap_or_else(|| "0.1.0".to_string());
        let mut s = format!("Package: {name}\nVersion: {version}\nType: Package\n");
        if let Some(d) = pkg.get("description") {
            s.push_str(&format!("Title: {d}\nDescription: {d}\n"));
        }
        if let Some(l) = pkg.get("license") {
            s.push_str(&format!("License: {l}\n"));
        }
        s.push_str("Authors@R: person(\"pleme-io\", role = c(\"aut\", \"cre\"))\n");
        s.push_str("Encoding: UTF-8\nRoxygenNote: 7.3.1\n");
        fs::write(&desc, s)?;
        written.push(desc);
    }
    render_ci_shims(c, out, force, &mut written)?;
    Ok(written)
}

fn render_rockspec(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
    let mut written = vec![];
    fs::create_dir_all(out)?;
    let pkg = &c.package;
    let name = pkg.get("name").cloned().unwrap_or_else(|| c.name.clone());
    let version = pkg.get("version").cloned().unwrap_or_else(|| "0.1.0".to_string());
    let rockspec = out.join(format!("{name}-{version}-1.rockspec"));
    if !rockspec.exists() || force {
        let mut s = format!("package = \"{name}\"\nversion = \"{version}-1\"\n");
        s.push_str("source = { url = \"\" }\n");
        s.push_str("description = {\n");
        if let Some(d) = pkg.get("description") { s.push_str(&format!("  summary = \"{d}\",\n")); }
        if let Some(l) = pkg.get("license") { s.push_str(&format!("  license = \"{l}\"\n")); }
        s.push_str("}\ndependencies = { \"lua >= 5.1\" }\n");
        s.push_str("build = { type = \"builtin\", modules = {} }\n");
        fs::write(&rockspec, s)?;
        written.push(rockspec);
    }
    render_ci_shims(c, out, force, &mut written)?;
    Ok(written)
}

fn render_conan(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
    let mut written = vec![];
    fs::create_dir_all(out)?;
    let conanfile = out.join("conanfile.py");
    if !conanfile.exists() || force {
        let pkg = &c.package;
        let name = pkg.get("name").cloned().unwrap_or_else(|| c.name.clone());
        let version = pkg.get("version").cloned().unwrap_or_else(|| "0.1.0".to_string());
        let class_name = name.replace('-', "_").split('_').map(|p| {
            let mut chars = p.chars();
            chars.next().map(|c| c.to_uppercase().chain(chars).collect::<String>()).unwrap_or_default()
        }).collect::<String>();
        let mut s = String::from("from conan import ConanFile\n\n");
        s.push_str(&format!("class {class_name}Conan(ConanFile):\n"));
        s.push_str(&format!("    name = \"{name}\"\n    version = \"{version}\"\n"));
        if let Some(l) = pkg.get("license") { s.push_str(&format!("    license = \"{l}\"\n")); }
        s.push_str("    settings = \"os\", \"compiler\", \"build_type\", \"arch\"\n");
        fs::write(&conanfile, s)?;
        written.push(conanfile);
    }
    render_ci_shims(c, out, force, &mut written)?;
    Ok(written)
}

// ─── M3.7 — wave 3: multi-PM + build-system extras ─────────────

fn render_conda(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
    use crate::yaml_ast::{render as render_yaml, Value as YVal};
    let mut written = vec![];
    fs::create_dir_all(out)?;
    let meta = out.join("meta.yaml");
    if !meta.exists() || force {
        let pkg = &c.package;
        let name = pkg.get("name").cloned().unwrap_or_else(|| c.name.clone());
        let version = pkg.get("version").cloned().unwrap_or_else(|| "0.1.0".to_string());

        let mut root = YVal::map();
        let mut package = YVal::map();
        package.insert("name", YVal::s(&name));
        package.insert("version", YVal::s(version));
        root.insert("package", package);

        let mut source = YVal::map();
        source.insert("path", YVal::s("."));
        root.insert("source", source);

        let mut build = YVal::map();
        build.insert("noarch", YVal::s("python"));
        build.insert("script", YVal::s("pip install . --no-deps"));
        root.insert("build", build);

        let mut requirements = YVal::map();
        requirements.insert("host", YVal::arr([YVal::s("python"), YVal::s("pip")]));
        requirements.insert("run", YVal::arr([YVal::s("python")]));
        root.insert("requirements", requirements);

        if let Some(d) = pkg.get("description") {
            let mut about = YVal::map();
            about.insert("summary", YVal::s(d));
            root.insert("about", about);
        }
        fs::write(&meta, render_yaml(&root))?;
        written.push(meta);
    }
    render_ci_shims(c, out, force, &mut written)?;
    Ok(written)
}

fn render_pipenv(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
    let mut written = vec![];
    fs::create_dir_all(out)?;
    let pipfile = out.join("Pipfile");
    if !pipfile.exists() || force {
        let mut s = String::from("[[source]]\nname = \"pypi\"\nurl = \"https://pypi.org/simple\"\nverify_ssl = true\n\n[packages]\n");
        let deps = parse_deps_raw(c.dependencies_raw.as_deref().unwrap_or(""));
        for d in &deps.runtime {
            if let (Some(n), Some(v)) = (d.get("name"), d.get("version")) {
                s.push_str(&format!("{n} = \"{v}\"\n"));
            }
        }
        s.push_str("\n[dev-packages]\n");
        for d in &deps.dev {
            if let (Some(n), Some(v)) = (d.get("name"), d.get("version")) {
                s.push_str(&format!("{n} = \"{v}\"\n"));
            }
        }
        fs::write(&pipfile, s)?;
        written.push(pipfile);
    }
    render_ci_shims(c, out, force, &mut written)?;
    Ok(written)
}

fn render_pdm(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
    // PDM uses pyproject.toml with [tool.pdm] — delegate to python emitter
    render_python(c, out, force)
}

fn render_deno(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
    use crate::json_ast::{render as render_json, Value as JVal};
    let mut written = vec![];
    fs::create_dir_all(out)?;
    let deno_json = out.join("deno.json");
    if !deno_json.exists() || force {
        let pkg = &c.package;
        let name = pkg.get("name").cloned().unwrap_or_else(|| c.name.clone());
        let version = pkg.get("version").cloned().unwrap_or_else(|| "0.1.0".to_string());

        let mut root = JVal::obj();
        // Scoped @pleme-io/<name> built by typed string concat (no fmt!() of code).
        let mut scoped = String::from("@pleme-io/");
        scoped.push_str(&name);
        root.insert("name", JVal::s(scoped));
        root.insert("version", JVal::s(version));
        root.insert("exports", JVal::s("./mod.ts"));

        let mut tasks = JVal::obj();
        tasks.insert("test", JVal::s("deno test"));
        root.insert("tasks", tasks);
        root.insert("imports", JVal::obj());

        fs::write(&deno_json, render_json(&root))?;
        written.push(deno_json);
    }
    render_ci_shims(c, out, force, &mut written)?;
    Ok(written)
}

fn render_pnpm(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
    use crate::yaml_ast::{render as render_yaml, Value as YVal};
    let mut written = render_npm(c, out, force)?;
    let pnpm_ws = out.join("pnpm-workspace.yaml");
    if !pnpm_ws.exists() || force {
        let mut root = YVal::map();
        root.insert("packages", YVal::arr([YVal::s("packages/*")]));
        fs::write(&pnpm_ws, render_yaml(&root))?;
        written.push(pnpm_ws);
    }
    Ok(written)
}

fn render_vcpkg(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
    use crate::json_ast::{render as render_json, Value as JVal};
    let mut written = vec![];
    fs::create_dir_all(out)?;
    let vcpkg = out.join("vcpkg.json");
    if !vcpkg.exists() || force {
        let pkg = &c.package;
        let name = pkg.get("name").cloned().unwrap_or_else(|| c.name.clone());
        let version = pkg.get("version").cloned().unwrap_or_else(|| "0.1.0".to_string());

        let mut root = JVal::obj();
        root.insert("name", JVal::s(name));
        root.insert("version-string", JVal::s(version));
        if let Some(d) = pkg.get("description") { root.insert("description", JVal::s(d)); }

        let deps = parse_deps_raw(c.dependencies_raw.as_deref().unwrap_or(""));
        let names: Vec<JVal> = deps.runtime.iter()
            .filter_map(|d| d.get("name").cloned())
            .map(JVal::s)
            .collect();
        root.insert("dependencies", JVal::Array(names));

        fs::write(&vcpkg, render_json(&root))?;
        written.push(vcpkg);
    }
    render_ci_shims(c, out, force, &mut written)?;
    Ok(written)
}

fn render_meson(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
    let mut written = vec![];
    fs::create_dir_all(out)?;
    let build = out.join("meson.build");
    if !build.exists() || force {
        let pkg = &c.package;
        let name = pkg.get("name").cloned().unwrap_or_else(|| c.name.clone());
        let version = pkg.get("version").cloned().unwrap_or_else(|| "0.1.0".to_string());
        let license = pkg.get("license").cloned().unwrap_or_else(|| "MIT".to_string());
        let mut s = format!("project('{name}', 'c', 'cpp',\n  version: '{version}',\n  license: '{license}',\n");
        s.push_str("  default_options: ['cpp_std=c++20'])\n\n");
        s.push_str(&format!("executable('{name}', 'src/main.cpp')\n"));
        fs::write(&build, s)?;
        written.push(build);
    }
    render_ci_shims(c, out, force, &mut written)?;
    Ok(written)
}

fn render_fpm(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
    use crate::toml_ast::{Document, Value as TVal};
    let mut written = vec![];
    fs::create_dir_all(out)?;
    let fpm = out.join("fpm.toml");
    if !fpm.exists() || force {
        let pkg = &c.package;
        let mut doc = Document::new();
        if let Some(n) = pkg.get("name") { doc.root_key("name", TVal::s(n)); }
        if let Some(v) = pkg.get("version") { doc.root_key("version", TVal::s(v)); }
        if let Some(l) = pkg.get("license") { doc.root_key("license", TVal::s(l)); }
        // [build] table — description goes in a typed string field, no
        // mid-section format!() of mixed-shape strings.
        let build = doc.table("build");
        build.key("auto-executables", TVal::b(true));
        build.key("auto-tests", TVal::b(true));
        if let Some(d) = pkg.get("description") {
            build.key("description", TVal::s(d));
        }
        fs::write(&fpm, doc.render())?;
        written.push(fpm);
    }
    render_ci_shims(c, out, force, &mut written)?;
    Ok(written)
}

fn render_gleam(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
    use crate::toml_ast::{Document, Value as TVal};
    let mut written = vec![];
    fs::create_dir_all(out)?;
    let g = out.join("gleam.toml");
    if !g.exists() || force {
        let pkg = &c.package;
        let mut doc = Document::new();
        if let Some(n) = pkg.get("name") { doc.root_key("name", TVal::s(n)); }
        if let Some(v) = pkg.get("version") { doc.root_key("version", TVal::s(v)); }
        if let Some(l) = pkg.get("license") {
            doc.root_key("licences", TVal::arr([TVal::s(l)]));
        }
        if let Some(d) = pkg.get("description") {
            doc.root_key("description", TVal::s(d));
        }
        doc.table("dependencies").key("gleam_stdlib", TVal::s(">= 0.34.0 and < 2.0.0"));
        doc.table("dev-dependencies").key("gleeunit", TVal::s(">= 1.0.0 and < 2.0.0"));
        fs::write(&g, doc.render())?;
        written.push(g);
    }
    render_ci_shims(c, out, force, &mut written)?;
    Ok(written)
}

fn render_alire(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
    use crate::toml_ast::{Document, Value as TVal};
    let mut written = vec![];
    fs::create_dir_all(out)?;
    let pkg = &c.package;
    let name = pkg.get("name").cloned().unwrap_or_else(|| c.name.clone());
    let a = out.join(format!("alire-{name}.toml"));
    if !a.exists() || force {
        let version = pkg.get("version").cloned().unwrap_or_else(|| "0.1.0".to_string());
        let mut doc = Document::new();
        doc.root_key("name", TVal::s(&name));
        doc.root_key("version", TVal::s(version));
        if let Some(d) = pkg.get("description") { doc.root_key("description", TVal::s(d)); }
        if let Some(l) = pkg.get("license") { doc.root_key("licenses", TVal::s(l)); }
        doc.root_key("authors", TVal::arr([TVal::s("pleme-io")]));
        doc.root_key("maintainers", TVal::arr([TVal::s("engineering@pleme.io")]));
        doc.root_key("maintainers-logins", TVal::arr([TVal::s("pleme-io")]));
        let mut s = doc.render();
        // [[depends-on]] is an array-of-tables — not yet modeled in
        // toml_ast::Document; emit a typed mini-table inline using the
        // toml_ast Value renderer for the GNAT constraint value.
        s.push_str("\n[[depends-on]]\ngnat = ");
        s.push_str(&TVal::s(">=11").render_inline());
        s.push('\n');
        fs::write(&a, s)?;
        written.push(a);
    }
    render_ci_shims(c, out, force, &mut written)?;
    Ok(written)
}

fn render_cabal(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
    let mut written = vec![];
    fs::create_dir_all(out)?;
    let pkg = &c.package;
    let name = pkg.get("name").cloned().unwrap_or_else(|| c.name.clone());
    let cab = out.join(format!("{name}.cabal"));
    if !cab.exists() || force {
        let version = pkg.get("version").cloned().unwrap_or_else(|| "0.1.0.0".to_string());
        let mut s = format!("cabal-version:      2.4\nname:               {name}\nversion:            {version}\n");
        if let Some(d) = pkg.get("description") { s.push_str(&format!("synopsis:           {d}\ndescription:        {d}\n")); }
        if let Some(l) = pkg.get("license") { s.push_str(&format!("license:            {l}\n")); }
        s.push_str("author:             pleme-io\nmaintainer:         engineering@pleme.io\nbuild-type:         Simple\n\n");
        s.push_str("library\n  exposed-modules:  Lib\n  hs-source-dirs:   src\n  build-depends:    base >= 4.14 && < 5\n  default-language: Haskell2010\n");
        fs::write(&cab, s)?;
        written.push(cab);
    }
    render_ci_shims(c, out, force, &mut written)?;
    Ok(written)
}

fn render_racket(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
    let mut written = vec![];
    fs::create_dir_all(out)?;
    let info = out.join("info.rkt");
    if !info.exists() || force {
        let pkg = &c.package;
        let name = pkg.get("name").cloned().unwrap_or_else(|| c.name.clone());
        let version = pkg.get("version").cloned().unwrap_or_else(|| "0.1.0".to_string());
        let mut s = format!("#lang info\n(define collection \"{name}\")\n(define version \"{version}\")\n(define deps '(\"base\"))\n");
        if let Some(d) = pkg.get("description") { s.push_str(&format!("(define pkg-desc \"{d}\")\n")); }
        s.push_str("(define pkg-authors '(pleme-io))\n");
        fs::write(&info, s)?;
        written.push(info);
    }
    render_ci_shims(c, out, force, &mut written)?;
    Ok(written)
}

// ─── M3.5 — code-as-manifest emitters ──────────────────────────

fn render_julia(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
    use crate::toml_ast::{Document, Value};
    let mut written = vec![];
    fs::create_dir_all(out)?;
    let project = out.join("Project.toml");
    if !project.exists() || force {
        let pkg = &c.package;
        let mut doc = Document::new();
        // M4.1 typed-AST emission. NO format!() of TOML source.
        // Per ★★★ NO-format-for-code directive.
        if let Some(n) = pkg.get("name") { doc.root_key("name", Value::s(n)); }
        // Julia requires UUID per package. Operator authors :uuid in :package.
        // Source-of-truth invariant: never generate random per render.
        let uuid = pkg.get("uuid").cloned()
            .unwrap_or_else(|| "00000000-0000-0000-0000-000000000000".to_string());
        doc.root_key("uuid", Value::s(uuid));
        if let Some(v) = pkg.get("version") { doc.root_key("version", Value::s(v)); }
        if let Some(authors) = pkg.get("authors") {
            doc.root_key("authors", Value::arr([Value::s(authors)]));
        }
        let deps = parse_deps_raw(c.dependencies_raw.as_deref().unwrap_or(""));
        if !deps.runtime.is_empty() {
            let deps_tbl = doc.table("deps");
            for d in &deps.runtime {
                if let (Some(n), Some(v)) = (d.get("name"), d.get("uuid")) {
                    deps_tbl.key(n, Value::s(v));
                }
            }
            let compat = doc.table("compat");
            if let Some(supports) = c.supports_raw.as_deref() {
                if let Some(jv) = read_keyword_in_block(supports, ":julia") {
                    compat.key("julia", Value::s(jv.trim_matches('"')));
                }
            }
        }
        fs::write(&project, doc.render())?;
        written.push(project);
    }
    render_ci_shims(c, out, force, &mut written)?;
    Ok(written)
}

#[derive(Debug, Default)]
pub struct Caixa {
    pub name: String,
    pub kind: String,
    pub ecosystem: String,
    pub package: BTreeMap<String, String>,
    pub package_lists: BTreeMap<String, Vec<String>>,
    pub workflows: Vec<String>,
    /// Composition stacks the caixa wires (e.g. :ai-stack, :data-stack,
    /// :compliance-attest, :observability-deploy, :web3-stack). Each
    /// renders as a job in the generated pleme-stacks.yml workflow.
    pub stacks: Vec<String>,
    /// Caixa-to-caixa dependencies (typed by repo + git ref).
    /// Format per entry: "pleme-io/foo@v1" or "pleme-io/foo@main".
    /// Renderer resolves these in the caixa-deps section of the
    /// adoption surface (clones the dep + adds to the build graph).
    pub depends_on: Vec<String>,
    /// What this caixa EXPOSES to other caixas (artifact types).
    /// E.g. ["rust-crate" "helm-chart" "docker-image" "tlisp-lib"].
    pub exposes: Vec<String>,
    /// When true: every push tags the caixa version (caixa.lisp's
    /// :version field) as a git tag, making the repo a git-as-package-repo
    /// for other caixas to :depends-on by tag.
    pub publish_to_git: bool,
    pub ci_config: BTreeMap<String, BTreeMap<String, String>>,

    // ── Best-of-package-managers schema (CAIXA-FULL-SCHEMA.md) ──
    pub dependencies_raw: Option<String>,
    pub features_raw: Option<String>,
    pub profiles_raw: Option<String>,
    pub scripts_raw: Option<String>,
    pub sources_raw: Option<String>,
    pub supports_raw: Option<String>,
    pub artifacts_raw: Option<String>,
    pub publish_raw: Option<String>,

    /// DEFAULT: true. When true, the renderer emits a Nix module
    /// trio (NixOS / Darwin / Home-Manager) under nix/modules/
    /// for any caixa or tatara-lisp artifact. Set to false to skip.
    pub nix_modules: bool,
    /// DEFAULT: true. When true, caixa-deps-resolve resolves
    /// transitive dependencies (a dep's :depends-on are also fetched).
    pub deps_transitive: bool,
    /// "local" | "nix" | "both". Default "both". Where to materialize
    /// resolved deps: directory tree (.caixa-deps/) and/or nix flake
    /// inputs (flake.nix).
    pub deps_sync_target: String,
}

/// Minimal s-expression parser sufficient for the (defcaixa ...)
/// shape. Returns a typed Caixa struct. Full M3 implementation
/// uses tatara-lisp-source's real parser; this is the bootstrap.
fn parse(src: &str) -> Result<Caixa> {
    // Strip comments
    let cleaned: String = src
        .lines()
        .map(|l| match l.find(";;") {
            Some(i) => &l[..i],
            None => l,
        })
        .collect::<Vec<_>>()
        .join("\n");

    let cleaned = cleaned.trim();
    if !cleaned.starts_with("(defcaixa") {
        bail!("source does not start with (defcaixa ...)");
    }

    // Extract the name (first token after defcaixa)
    let after_keyword = &cleaned[("(defcaixa".len())..].trim_start();
    let name_end = after_keyword
        .find(|c: char| c.is_whitespace())
        .unwrap_or(after_keyword.len());
    let name = after_keyword[..name_end].to_string();

    // Naive slot extraction
    let body = &after_keyword[name_end..];
    let mut out = Caixa {
        name,
        // Defaults per CAIXA-FULL-SCHEMA.md: nix-modules + deps-transitive
        // + deps-sync-target "both" are ON by default. Operator opts out
        // explicitly when needed.
        nix_modules: true,
        deps_transitive: true,
        deps_sync_target: "both".to_string(),
        ..Default::default()
    };

    out.kind = read_keyword(body, ":kind").unwrap_or_default();
    out.ecosystem = read_keyword(body, ":ecosystem").unwrap_or_default();

    if let Some(pkg_block) = read_dict_block(body, ":package") {
        out.package = parse_dict(&pkg_block);
        // Categories + keywords come back as Vec<String>
        if let Some(cats) = read_vector_in_dict(&pkg_block, ":categories") {
            out.package_lists.insert("categories".into(), cats);
        }
        if let Some(kws) = read_vector_in_dict(&pkg_block, ":keywords") {
            out.package_lists.insert("keywords".into(), kws);
        }
    }

    if let Some(wf) = read_vector(body, ":workflows") {
        out.workflows = wf;
    }
    if let Some(stacks) = read_vector(body, ":stacks") {
        out.stacks = stacks;
    }
    if let Some(deps) = read_vector(body, ":depends-on") {
        out.depends_on = deps;
    }
    if let Some(exposes) = read_vector(body, ":exposes") {
        out.exposes = exposes;
    }
    if let Some(pub_to_git) = read_keyword(body, ":publish-to-git") {
        out.publish_to_git = pub_to_git == "true";
    }

    // Overrides for defaults
    if let Some(nm) = read_keyword(body, ":nix-modules") {
        out.nix_modules = nm != "false";
    }
    if let Some(t) = read_keyword(body, ":deps-transitive") {
        out.deps_transitive = t != "false";
    }
    if let Some(s) = read_keyword(body, ":deps-sync-target") {
        out.deps_sync_target = s;
    }

    // Best-of-PM schema (parsed raw; emitters land per consumer demand)
    out.dependencies_raw = read_dict_block(body, ":dependencies");
    out.features_raw     = read_dict_block(body, ":features");
    out.profiles_raw     = read_dict_block(body, ":profiles");
    out.scripts_raw      = read_dict_block(body, ":scripts");
    out.sources_raw      = read_dict_block(body, ":sources");
    out.supports_raw     = read_dict_block(body, ":supports");
    out.artifacts_raw    = read_dict_block(body, ":artifacts");
    out.publish_raw      = read_dict_block(body, ":publish");

    if let Some(ci_block) = read_dict_block(body, ":ci-config") {
        for sub_key in ["bump", "publish", "security", "validation"] {
            if let Some(sub_block) = read_dict_block(&ci_block, &format!(":{sub_key}")) {
                out.ci_config.insert(sub_key.into(), parse_dict(&sub_block));
            }
        }
    }

    Ok(out)
}

/// Find `kw` followed by whitespace (any amount). Returns the
/// slice AFTER the whitespace block.
fn find_after_keyword<'a>(s: &'a str, kw: &str) -> Option<&'a str> {
    let mut start = 0;
    while let Some(i) = s[start..].find(kw) {
        let abs = start + i;
        let after_kw = &s[abs + kw.len()..];
        // Must be followed by whitespace
        if after_kw.starts_with(|c: char| c.is_whitespace()) {
            return Some(after_kw.trim_start());
        }
        start = abs + kw.len();
    }
    None
}

fn read_keyword(s: &str, kw: &str) -> Option<String> {
    let after = find_after_keyword(s, kw)?;
    let end = after
        .find(|c: char| c.is_whitespace() || c == ')' || c == '}')
        .unwrap_or(after.len());
    Some(after[..end].trim_matches(':').to_string())
}

fn read_dict_block(s: &str, kw: &str) -> Option<String> {
    let after = find_after_keyword(s, kw)?;
    let after = after.trim_start();
    if !after.starts_with('{') {
        return None;
    }
    let mut depth = 0;
    let mut end = 0;
    for (j, c) in after.char_indices() {
        match c {
            '{' => depth += 1,
            '}' => {
                depth -= 1;
                if depth == 0 {
                    end = j + 1;
                    break;
                }
            }
            _ => {}
        }
    }
    if end == 0 {
        return None;
    }
    // Inner content between { and }
    Some(after[1..end - 1].to_string())
}

fn read_vector(s: &str, kw: &str) -> Option<Vec<String>> {
    let after = find_after_keyword(s, kw)?;
    if !after.starts_with('[') {
        return None;
    }
    let mut depth = 0;
    let mut end = 0;
    for (j, c) in after.char_indices() {
        match c {
            '[' => depth += 1,
            ']' => {
                depth -= 1;
                if depth == 0 {
                    end = j + 1;
                    break;
                }
            }
            _ => {}
        }
    }
    if end == 0 {
        return None;
    }
    let inner = &after[1..end - 1];
    Some(
        inner
            .split_whitespace()
            .map(|w| w.trim_matches('"').trim_start_matches(':').to_string())
            .filter(|s| !s.is_empty())
            .collect(),
    )
}

fn read_vector_in_dict(dict: &str, kw: &str) -> Option<Vec<String>> {
    read_vector(dict, kw)
}

/// Parse a {:k v :k v ...} dict's inner content into a flat map.
/// Strings only; nested dicts/vectors handled separately.
fn parse_dict(inner: &str) -> BTreeMap<String, String> {
    let mut out = BTreeMap::new();
    let mut chars = inner.char_indices().peekable();
    while let Some((i, c)) = chars.next() {
        if c == ':' {
            // Read keyword
            let mut end = i + 1;
            while end < inner.len() && !inner.as_bytes()[end].is_ascii_whitespace() {
                end += 1;
            }
            let kw = inner[i + 1..end].to_string();
            // Skip whitespace + read value
            let mut val_start = end;
            while val_start < inner.len() && inner.as_bytes()[val_start].is_ascii_whitespace() {
                val_start += 1;
            }
            if val_start >= inner.len() {
                break;
            }
            // Skip nested dicts/vectors for now (handled separately)
            if inner.as_bytes()[val_start] == b'{' || inner.as_bytes()[val_start] == b'[' {
                // Advance chars past this nested struct
                let mut depth = 0;
                let open = inner.as_bytes()[val_start] as char;
                let close = if open == '{' { '}' } else { ']' };
                let mut p = val_start;
                for (j, c2) in inner[val_start..].char_indices() {
                    match c2 {
                        c if c == open => depth += 1,
                        c if c == close => {
                            depth -= 1;
                            if depth == 0 {
                                p = val_start + j + 1;
                                break;
                            }
                        }
                        _ => {}
                    }
                }
                // Advance the outer iterator
                while let Some(&(k, _)) = chars.peek() {
                    if k >= p {
                        break;
                    }
                    chars.next();
                }
                continue;
            }
            // String value
            if inner.as_bytes()[val_start] == b'"' {
                let mut end = val_start + 1;
                while end < inner.len() && inner.as_bytes()[end] != b'"' {
                    end += 1;
                }
                out.insert(kw, inner[val_start + 1..end].to_string());
                while let Some(&(k, _)) = chars.peek() {
                    if k > end {
                        break;
                    }
                    chars.next();
                }
            } else {
                // Bare token (keyword / number / true / false)
                let mut end = val_start;
                while end < inner.len() && !inner.as_bytes()[end].is_ascii_whitespace() {
                    end += 1;
                }
                out.insert(kw, inner[val_start..end].trim_start_matches(':').to_string());
                while let Some(&(k, _)) = chars.peek() {
                    if k >= end {
                        break;
                    }
                    chars.next();
                }
            }
        }
    }
    out
}

fn render_rust_single(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
    use crate::toml_ast::{Document, Value as TVal};
    let mut written = vec![];
    fs::create_dir_all(out)?;
    let wf_dir = out.join(".github/workflows");
    fs::create_dir_all(&wf_dir)?;

    // 1. Cargo.toml — typed toml_ast emission. Per ★★★ NO-format!()-
    //    for-code: every value flows through render_basic_string in
    //    toml_ast (quote escaping by construction).
    let cargo_path = out.join("Cargo.toml");
    if !cargo_path.exists() || force {
        let pkg = &c.package;
        let mut doc = Document::new();

        // [package]
        {
            let p = doc.table("package");
            if let Some(n) = pkg.get("name") { p.key("name", TVal::s(n)); }
            if let Some(v) = pkg.get("version") { p.key("version", TVal::s(v)); }
            p.key("edition", TVal::s("2024"));
            if let Some(supports) = c.supports_raw.as_deref() {
                if let Some(rust_req) = read_keyword_in_block(supports, ":rust") {
                    let clean = rust_req.trim_matches('"').trim_start_matches(">=").to_string();
                    p.key("rust-version", TVal::s(clean));
                }
            }
            if let Some(d) = pkg.get("description") { p.key("description", TVal::s(d)); }
            if let Some(l) = pkg.get("license") { p.key("license", TVal::s(l)); }
            if let Some(r) = pkg.get("repository") { p.key("repository", TVal::s(r)); }
            if let Some(h) = pkg.get("homepage") { p.key("homepage", TVal::s(h)); }
            p.key("readme", TVal::s("README.md"));
            let authors = pkg.get("authors")
                .cloned()
                .unwrap_or_else(|| "pleme-io".to_string());
            p.key("authors", TVal::arr([TVal::s(authors)]));
            if let Some(cats) = c.package_lists.get("categories") {
                p.key("categories", TVal::arr(cats.iter().map(TVal::s)));
            }
            if let Some(kws) = c.package_lists.get("keywords") {
                p.key("keywords", TVal::arr(kws.iter().map(TVal::s)));
            }
        }

        let typed_cargo = doc.render();

        // Render the typed backbone first, then append the remaining
        // shapes (features dict, profiles dict, publish array, [[bin]]
        // multi-tables) via the still-bespoke emitters. These will move
        // into toml_ast in a follow-up commit once the Document type
        // grows array-of-tables + nested-table support.
        let mut cargo = typed_cargo;

        // :artifacts.bin → [[bin]] tables (array-of-tables shape not yet
        //                  modeled in toml_ast::Document)
        if let Some(artifacts) = c.artifacts_raw.as_deref() {
            for bin in extract_bin_entries(artifacts) {
                cargo.push_str("\n[[bin]]\n");
                if let Some(n) = bin.get("name") {
                    let _ = writeln!(cargo, "name = {}", TVal::s(n).render_inline());
                }
                if let Some(p) = bin.get("path") {
                    let _ = writeln!(cargo, "path = {}", TVal::s(p).render_inline());
                }
            }
        }

        // :publish.* → package-level publish fields (inside [package])
        if let Some(publish) = c.publish_raw.as_deref() {
            let publish_fields = emit_publish_fields(publish);
            if !publish_fields.is_empty() {
                cargo.push_str(&publish_fields);
            }
        }

        // [lints.clippy] — typed mini-table inline.
        {
            let mut lints_doc = Document::new();
            lints_doc.table("lints.clippy").key("pedantic", TVal::s("warn"));
            cargo.push('\n');
            cargo.push_str(&lints_doc.render());
        }

        // :profiles → [profile.<name>] tables — bespoke for now (nested
        //              [profile.release] header form).
        if let Some(profiles) = c.profiles_raw.as_deref() {
            cargo.push_str(&emit_profiles_blocks(profiles));
        }

        // :features → [features] table — bespoke for now (each key is an
        //              array-of-strings authored by name in source order).
        if let Some(features) = c.features_raw.as_deref() {
            let features_block = emit_features_block(features);
            if !features_block.is_empty() {
                cargo.push_str("\n[features]\n");
                cargo.push_str(&features_block);
            }
        }

        // :dependencies → [dependencies] + [dev-dependencies] +
        //                  [build-dependencies] — bespoke for now (deps
        //                  with optional inline-tables {version=, features=}).
        let deps = parse_deps_raw(c.dependencies_raw.as_deref().unwrap_or(""));
        cargo.push_str("\n[dependencies]\n");
        for d in &deps.runtime {
            cargo.push_str(&emit_cargo_dep(d));
        }
        if !deps.dev.is_empty() {
            cargo.push_str("\n[dev-dependencies]\n");
            for d in &deps.dev {
                cargo.push_str(&emit_cargo_dep(d));
            }
        }
        if !deps.build.is_empty() {
            cargo.push_str("\n[build-dependencies]\n");
            for d in &deps.build {
                cargo.push_str(&emit_cargo_dep(d));
            }
        }
        fs::write(&cargo_path, cargo)?;
        written.push(cargo_path);
    }

    // 2. .pleme-io-release.toml
    let cfg_path = out.join(".pleme-io-release.toml");
    if !cfg_path.exists() || force {
        let mut cfg = String::from("# Generated by pleme-doc-gen caixa\n");
        for (section, fields) in &c.ci_config {
            cfg.push_str(&format!("[{section}]\n"));
            for (k, v) in fields {
                let v = if matches!(v.as_str(), "true" | "false") {
                    v.clone()
                } else {
                    format!("\"{v}\"")
                };
                cfg.push_str(&format!("{k} = {v}\n"));
            }
            cfg.push('\n');
        }
        fs::write(&cfg_path, cfg)?;
        written.push(cfg_path);
    }

    // 3+. CI shims
    for wf in &c.workflows {
        let path = wf_dir.join(format!("{wf}.yml"));
        if path.exists() && !force {
            continue;
        }
        let content = match wf.as_str() {
            "auto-release" => AUTO_RELEASE_SHIM,
            "pre-merge-gate" => PRE_MERGE_GATE_SHIM,
            "security-gate" => SECURITY_GATE_SHIM,
            _ => continue,
        };
        fs::write(&path, content)?;
        written.push(path);
    }
    emit_stacks_workflow(c, &wf_dir, force, &mut written)?;
    if c.nix_modules {
        emit_nix_module_trio(c, out, force, &mut written)?;
    }
    emit_deps_sync(c, out, force, &mut written)?;
    emit_flake_nix(c, out, force, &mut written)?;

    Ok(written)
}

/// Shared: emit .pleme-io-release.toml + CI shims for any ecosystem.
fn render_ci_shims(c: &Caixa, out: &Path, force: bool, written: &mut Vec<PathBuf>) -> Result<()> {
    let wf_dir = out.join(".github/workflows");
    fs::create_dir_all(&wf_dir)?;

    let cfg_path = out.join(".pleme-io-release.toml");
    if !cfg_path.exists() || force {
        let mut cfg = String::from("# Generated by pleme-doc-gen caixa\n");
        for (section, fields) in &c.ci_config {
            cfg.push_str(&format!("[{section}]\n"));
            for (k, v) in fields {
                let v = if matches!(v.as_str(), "true" | "false") {
                    v.clone()
                } else {
                    format!("\"{v}\"")
                };
                cfg.push_str(&format!("{k} = {v}\n"));
            }
            cfg.push('\n');
        }
        fs::write(&cfg_path, cfg)?;
        written.push(cfg_path);
    }

    for wf in &c.workflows {
        let path = wf_dir.join(format!("{wf}.yml"));
        if path.exists() && !force {
            continue;
        }
        let content = match wf.as_str() {
            "auto-release" => AUTO_RELEASE_SHIM,
            "pre-merge-gate" => PRE_MERGE_GATE_SHIM,
            "security-gate" => SECURITY_GATE_SHIM,
            _ => continue,
        };
        fs::write(&path, content)?;
        written.push(path);
    }
    emit_stacks_workflow(c, &wf_dir, force, written)?;
    let out_dir = wf_dir.parent().unwrap().parent().unwrap().to_path_buf();
    if c.nix_modules {
        emit_nix_module_trio(c, &out_dir, force, written)?;
    }
    emit_deps_sync(c, &out_dir, force, written)?;
    emit_flake_nix(c, &out_dir, force, written)?;
    Ok(())
}

/// Read a keyword value out of an already-extracted block.
fn read_keyword_in_block(block: &str, kw: &str) -> Option<String> {
    read_keyword(block, kw)
}

#[derive(Debug, Default)]
struct CaixaDeps {
    runtime: Vec<BTreeMap<String, String>>,
    dev: Vec<BTreeMap<String, String>>,
    build: Vec<BTreeMap<String, String>>,
}

/// Parse :dependencies raw block into structured per-class deps.
/// Each dep is a dict like { :name "X" :version "Y" :features [...] }.
fn parse_deps_raw(raw: &str) -> CaixaDeps {
    let mut out = CaixaDeps::default();
    for kw in [":runtime", ":dev", ":build"] {
        if let Some(vec_src) = extract_vector_block(raw, kw) {
            let class_deps = parse_dep_vector(&vec_src);
            match kw {
                ":runtime" => out.runtime = class_deps,
                ":dev" => out.dev = class_deps,
                ":build" => out.build = class_deps,
                _ => {}
            }
        }
    }
    out
}

/// Find ` :keyword [ ... ]` and return the inner content.
fn extract_vector_block(s: &str, kw: &str) -> Option<String> {
    let after = find_after_keyword(s, kw)?;
    if !after.starts_with('[') {
        return None;
    }
    let mut depth = 0;
    let mut end = 0;
    for (j, c) in after.char_indices() {
        match c {
            '[' => depth += 1,
            ']' => {
                depth -= 1;
                if depth == 0 {
                    end = j + 1;
                    break;
                }
            }
            _ => {}
        }
    }
    if end == 0 {
        return None;
    }
    Some(after[1..end - 1].to_string())
}

/// Parse a vector of dep dicts: `{ :name "X" :version "Y" } { :name "Z" ... }`
fn parse_dep_vector(s: &str) -> Vec<BTreeMap<String, String>> {
    let mut out = vec![];
    let mut depth = 0;
    let mut start = None;
    for (i, c) in s.char_indices() {
        match c {
            '{' => {
                if depth == 0 {
                    start = Some(i + 1);
                }
                depth += 1;
            }
            '}' => {
                depth -= 1;
                if depth == 0 {
                    if let Some(s_start) = start {
                        out.push(parse_dict(&s[s_start..i]));
                        start = None;
                    }
                }
            }
            _ => {}
        }
    }
    out
}

fn emit_cargo_dep(d: &BTreeMap<String, String>) -> String {
    let name = d.get("name").cloned().unwrap_or_default();
    let version = d.get("version").cloned().unwrap_or_else(|| "*".to_string());
    if name.is_empty() {
        return String::new();
    }
    // Check for :features in the dep spec — if present, emit inline-table form.
    if let Some(feats_raw) = d.get("features") {
        // features stored as "[ a b c ]" — parse the vec
        let feats: Vec<String> = feats_raw
            .trim_start_matches('[')
            .trim_end_matches(']')
            .split_whitespace()
            .map(|s| s.trim_matches('"').trim_start_matches(':').to_string())
            .filter(|s| !s.is_empty())
            .collect();
        if !feats.is_empty() {
            let joined: Vec<String> = feats.iter().map(|f| format!("\"{f}\"")).collect();
            return format!("{name} = {{ version = \"{version}\", features = [{}] }}\n", joined.join(", "));
        }
    }
    format!("{name} = \"{version}\"\n")
}

/// Parse :features { :default [...] :foo { :adds [...] } } → printable
/// [features] block content.
fn emit_features_block(features_raw: &str) -> String {
    let mut out = String::new();
    // Iterate sub-keywords inside the features dict
    let mut chars = features_raw.char_indices().peekable();
    let mut current_key: Option<String> = None;
    while let Some((i, c)) = chars.next() {
        if c == ':' {
            // Read keyword name
            let mut end = i + 1;
            while end < features_raw.len() && !features_raw.as_bytes()[end].is_ascii_whitespace() {
                end += 1;
            }
            current_key = Some(features_raw[i + 1..end].to_string());
            // Skip whitespace
            while let Some(&(k, ch)) = chars.peek() {
                if k <= end || ch.is_whitespace() {
                    chars.next();
                } else {
                    break;
                }
            }
            // What follows is either a [ vec ] (simple list) or a { :adds [...] } dict
            if let Some(&(_, ch)) = chars.peek() {
                if ch == '[' {
                    // Simple feature: name = [...]
                    let rest = &features_raw[end..];
                    if let Some(open) = rest.find('[') {
                        let mut depth = 0;
                        let mut close = 0;
                        for (j, c) in rest[open..].char_indices() {
                            match c {
                                '[' => depth += 1,
                                ']' => {
                                    depth -= 1;
                                    if depth == 0 {
                                        close = open + j + 1;
                                        break;
                                    }
                                }
                                _ => {}
                            }
                        }
                        if close > 0 {
                            let inner = &rest[open + 1..close - 1];
                            let items: Vec<String> = inner
                                .split_whitespace()
                                .map(|s| format!("\"{}\"", s.trim_matches('"').trim_start_matches(':')))
                                .filter(|s| s != "\"\"")
                                .collect();
                            if let Some(key) = &current_key {
                                out.push_str(&format!("{key} = [{}]\n", items.join(", ")));
                            }
                            // Advance chars past close
                            while let Some(&(k, _)) = chars.peek() {
                                if k >= end + close {
                                    break;
                                }
                                chars.next();
                            }
                        }
                    }
                } else if ch == '{' {
                    // Adds-form: name = { adds = [...] } → emit name = [adds]
                    let after = &features_raw[end..];
                    // Find the matching closing brace + advance chars past it
                    if let Some(open) = after.find('{') {
                        let mut depth = 0;
                        let mut close = 0;
                        for (j, c) in after[open..].char_indices() {
                            match c {
                                '{' => depth += 1,
                                '}' => {
                                    depth -= 1;
                                    if depth == 0 {
                                        close = open + j + 1;
                                        break;
                                    }
                                }
                                _ => {}
                            }
                        }
                        if close > 0 {
                            let dict_inner = &after[open + 1..close - 1];
                            if let Some(adds_vec) = extract_vector_block(dict_inner, ":adds") {
                                let items: Vec<String> = adds_vec
                                    .split_whitespace()
                                    .map(|s| format!("\"{}\"", s.trim_matches('"').trim_start_matches(':')))
                                    .filter(|s| s != "\"\"")
                                    .collect();
                                if let Some(key) = &current_key {
                                    out.push_str(&format!("{key} = [{}]\n", items.join(", ")));
                                }
                            }
                            // Advance chars past the dict close
                            let abs_close = end + close;
                            while let Some(&(k, _)) = chars.peek() {
                                if k >= abs_close {
                                    break;
                                }
                                chars.next();
                            }
                        }
                    }
                }
            }
        }
    }
    out
}

/// Parse :profiles { :release { :opt-level 3 :lto true ... } ... } →
/// printable Cargo [profile.<name>] tables.
fn emit_profiles_blocks(profiles_raw: &str) -> String {
    let mut out = String::new();
    for profile_name in ["release", "dev", "bench", "test"] {
        let kw = format!(":{profile_name}");
        if let Some(block) = read_dict_block(profiles_raw, &kw) {
            out.push_str(&format!("\n[profile.{profile_name}]\n"));
            let fields = parse_dict(&block);
            for (k, v) in &fields {
                let val = if matches!(v.as_str(), "true" | "false")
                    || v.parse::<i64>().is_ok()
                {
                    v.clone()
                } else {
                    format!("\"{v}\"")
                };
                out.push_str(&format!("{k} = {val}\n"));
            }
        }
    }
    out
}

/// Parse :publish { :private bool :files-include [...] ... } → printable
/// Cargo [package] publish fields (which go inside [package] itself).
fn emit_publish_fields(publish_raw: &str) -> String {
    let mut out = String::new();
    let fields = parse_dict(publish_raw);
    if let Some(v) = fields.get("private") {
        if v == "true" {
            out.push_str("publish = false\n");
        }
    }
    if let Some(incl) = extract_vector_block(publish_raw, ":files-include") {
        let items: Vec<String> = incl
            .split_whitespace()
            .map(|s| format!("\"{}\"", s.trim_matches('"')))
            .filter(|s| s != "\"\"")
            .collect();
        if !items.is_empty() {
            out.push_str(&format!("include = [{}]\n", items.join(", ")));
        }
    }
    if let Some(excl) = extract_vector_block(publish_raw, ":files-exclude") {
        let items: Vec<String> = excl
            .split_whitespace()
            .map(|s| format!("\"{}\"", s.trim_matches('"')))
            .filter(|s| s != "\"\"")
            .collect();
        if !items.is_empty() {
            out.push_str(&format!("exclude = [{}]\n", items.join(", ")));
        }
    }
    out
}

/// Pull entries from :artifacts.bin which is a Vec of { :name "X" :path "Y" }
fn extract_bin_entries(artifacts: &str) -> Vec<BTreeMap<String, String>> {
    extract_vector_block(artifacts, ":bin")
        .map(|v| parse_dep_vector(&v))
        .unwrap_or_default()
}

/// Emit nix/modules/{nixos,darwin,home-manager}.nix — the typed
/// module trio every caixa ships by default per the user's
/// 'autogenerate fully nixos/darwin-nix/home-manager modules ...
/// by default' directive.
fn emit_nix_module_trio(
    c: &Caixa,
    out: &Path,
    force: bool,
    written: &mut Vec<PathBuf>,
) -> Result<()> {
    let mod_dir = out.join("nix/modules");
    fs::create_dir_all(&mod_dir)?;
    let name = &c.name;
    let desc = c.package.get("description").cloned().unwrap_or_default();

    let nixos = format!(
        "# nix/modules/nixos.nix — auto-generated from {name}.caixa.lisp
# {desc}
{{ config, lib, pkgs, ... }}:
let
  cfg = config.services.{name};
in {{
  options.services.{name} = {{
    enable = lib.mkEnableOption \"{name}\";
    package = lib.mkOption {{
      type = lib.types.package;
      default = pkgs.{name} or null;
    }};
  }};
  config = lib.mkIf cfg.enable {{
    environment.systemPackages = [ cfg.package ];
  }};
}}
"
    );
    let darwin = format!(
        "# nix/modules/darwin.nix — auto-generated from {name}.caixa.lisp
{{ config, lib, pkgs, ... }}:
let cfg = config.services.{name}; in {{
  options.services.{name} = {{
    enable = lib.mkEnableOption \"{name}\";
    package = lib.mkOption {{ type = lib.types.package; default = pkgs.{name} or null; }};
  }};
  config = lib.mkIf cfg.enable {{
    environment.systemPackages = [ cfg.package ];
  }};
}}
"
    );
    let hm = format!(
        "# nix/modules/home-manager.nix — auto-generated from {name}.caixa.lisp
{{ config, lib, pkgs, ... }}:
let cfg = config.programs.{name}; in {{
  options.programs.{name} = {{
    enable = lib.mkEnableOption \"{name}\";
    package = lib.mkOption {{ type = lib.types.package; default = pkgs.{name} or null; }};
  }};
  config = lib.mkIf cfg.enable {{ home.packages = [ cfg.package ]; }};
}}
"
    );

    for (file, content) in [("nixos.nix", nixos), ("darwin.nix", darwin), ("home-manager.nix", hm)] {
        let p = mod_dir.join(file);
        if !p.exists() || force {
            fs::write(&p, content)?;
            written.push(p);
        }
    }
    Ok(())
}

/// Emit nix/caixa-deps.nix (flake-input fragment) + .caixa-deps/MANIFEST.txt
/// (local-sync manifest). Per :deps-sync-target (local | nix | both).
fn emit_deps_sync(
    c: &Caixa,
    out: &Path,
    force: bool,
    written: &mut Vec<PathBuf>,
) -> Result<()> {
    if c.depends_on.is_empty() {
        return Ok(());
    }
    let sync = c.deps_sync_target.as_str();
    if matches!(sync, "nix" | "both") {
        let nix_fragment = out.join("nix/caixa-deps.nix");
        fs::create_dir_all(out.join("nix"))?;
        if !nix_fragment.exists() || force {
            let mut s = String::from("# Auto-generated from caixa :depends-on.\n");
            s.push_str("# Splice into flake.nix `inputs = { ... };` block.\n{\n");
            for dep in &c.depends_on {
                let parts: Vec<&str> = dep.splitn(2, '@').collect();
                let repo = parts[0];
                let rev = parts.get(1).copied().unwrap_or("main");
                let key = repo.rsplit('/').next().unwrap_or("dep").replace('-', "_");
                s.push_str(&format!(
                    "  {key} = {{ url = \"github:{repo}/{rev}\"; flake = true; }};\n"
                ));
            }
            s.push_str("}\n");
            fs::write(&nix_fragment, s)?;
            written.push(nix_fragment);
        }
    }
    if matches!(sync, "local" | "both") {
        let manifest = out.join(".caixa-deps").join("MANIFEST.txt");
        fs::create_dir_all(out.join(".caixa-deps"))?;
        if !manifest.exists() || force {
            let mut s = String::from("# Auto-generated from caixa :depends-on.\n");
            for dep in &c.depends_on {
                s.push_str(&format!("{dep}\n"));
            }
            if c.deps_transitive {
                s.push_str("\n# :deps-transitive=true — caixa-deps-resolve recurses.\n");
            }
            fs::write(&manifest, s)?;
            written.push(manifest);
        }
    }
    Ok(())
}

/// Emit flake.nix consuming substrate's per-ecosystem builders.
/// Per the user's directive: every caixa auto-generates its
/// flake.nix as the operator-facing Nix entry point.
fn emit_flake_nix(
    c: &Caixa,
    out: &Path,
    force: bool,
    written: &mut Vec<PathBuf>,
) -> Result<()> {
    let path = out.join("flake.nix");
    if path.exists() && !force {
        return Ok(());
    }
    let name = &c.name;
    let mut inputs = String::from(
        "    nixpkgs.url = \"github:NixOS/nixpkgs/nixos-unstable\";\n    \
         substrate = {\n      url = \"github:pleme-io/substrate\";\n      \
         inputs.nixpkgs.follows = \"nixpkgs\";\n    };\n    \
         flake-parts.url = \"github:hercules-ci/flake-parts\";\n",
    );
    for dep in &c.depends_on {
        let parts: Vec<&str> = dep.splitn(2, '@').collect();
        let repo = parts[0];
        let rev = parts.get(1).copied().unwrap_or("main");
        let key = repo.rsplit('/').next().unwrap_or("dep").replace('-', "_");
        inputs.push_str(&format!(
            "    {key} = {{\n      url = \"github:{repo}/{rev}\";\n      \
             inputs.nixpkgs.follows = \"nixpkgs\";\n    }};\n"
        ));
    }
    let builder = match c.ecosystem.as_str() {
        "rust-single-crate" => "substrate.lib.${system}.rustToolReleaseFlakeBuilder",
        "rust-workspace" => "substrate.lib.${system}.rustWorkspaceReleaseFlakeBuilder",
        "npm" => "substrate.lib.${system}.typescriptToolFlakeBuilder",
        "python" => "substrate.lib.${system}.pythonToolFlakeBuilder",
        "helm" => "substrate.lib.${system}.helmChartFlakeBuilder",
        "github-action" => "substrate.lib.${system}.rustToolReleaseFlakeBuilder",
        _ => "substrate.lib.${system}.rustToolReleaseFlakeBuilder",
    };
    let content = format!(
        "# flake.nix — auto-generated from {name}.caixa.lisp
# Edit caixa source + re-render via:
#   pleme-doc-gen caixa --source {name}.caixa.lisp --out . --force
{{
  description = \"{name} — caixa-rendered Nix flake\";

  inputs = {{
{inputs}  }};

  outputs = inputs @ {{ self, nixpkgs, substrate, flake-parts, ... }}:
    flake-parts.lib.mkFlake {{ inherit inputs; }} {{
      systems = [ \"aarch64-darwin\" \"x86_64-linux\" \"aarch64-linux\" ];
      perSystem = {{ pkgs, system, ... }}: let
        builder = {builder};
      in {{
        packages.default = builder {{
          inherit pkgs;
          name = \"{name}\";
          src = ./.;
        }};
        devShells.default = pkgs.mkShell {{
          buildInputs = [ pkgs.git ];
        }};
      }};
    }};
}}
"
    );
    fs::write(&path, content)?;
    written.push(path);
    Ok(())
}

/// Emit pleme-stacks.yml if any :stacks declared. ONE workflow
/// file wires every composition stack the caixa opts into.
fn emit_stacks_workflow(
    c: &Caixa,
    wf_dir: &Path,
    force: bool,
    written: &mut Vec<PathBuf>,
) -> Result<()> {
    if c.stacks.is_empty() {
        return Ok(());
    }
    let stacks_path = wf_dir.join("pleme-stacks.yml");
    if stacks_path.exists() && !force {
        return Ok(());
    }
    let mut content = String::from(
        "name: pleme-stacks\n# Auto-generated from caixa :stacks slot.\n\n",
    );
    content.push_str("on:\n  push: { branches: [main] }\n  pull_request: { branches: [main] }\n\n");
    content.push_str("permissions:\n  contents: read\n  packages: write\n  id-token: write\n\n");
    content.push_str("jobs:\n");
    for stack in &c.stacks {
        let job_name = stack.replace('-', "_");
        content.push_str(&format!("  {job_name}:\n"));
        content.push_str(&format!(
            "    uses: pleme-io/substrate/.github/workflows/{stack}.yml@main\n"
        ));
        content.push_str("    secrets: inherit\n");
    }
    fs::write(&stacks_path, content)?;
    written.push(stacks_path.to_path_buf());
    Ok(())
}

fn render_npm(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
    use crate::json_ast::{render as render_json, Value};
    let mut written = vec![];
    fs::create_dir_all(out)?;

    let pkg_json = out.join("package.json");
    if !pkg_json.exists() || force {
        let pkg = &c.package;
        let mut json = Value::obj();
        if let Some(n) = pkg.get("name") { json.insert("name", Value::s(n)); }
        if let Some(v) = pkg.get("version") { json.insert("version", Value::s(v)); }
        if let Some(d) = pkg.get("description") { json.insert("description", Value::s(d)); }
        if let Some(l) = pkg.get("license") { json.insert("license", Value::s(l)); }
        if let Some(r) = pkg.get("repository") {
            let mut repo_obj = Value::obj();
            repo_obj.insert("type", Value::s("git"));
            repo_obj.insert("url", Value::s(r));
            json.insert("repository", repo_obj);
        }
        if let Some(kws) = c.package_lists.get("keywords") {
            json.insert("keywords", Value::arr(kws.iter().map(Value::s)));
        }
        if let Some(supports) = c.supports_raw.as_deref() {
            if let Some(node_req) = read_keyword_in_block(supports, ":node") {
                let mut engines = Value::obj();
                engines.insert("node", Value::s(node_req.trim_matches('"')));
                json.insert("engines", engines);
            }
        }
        if let Some(publish) = c.publish_raw.as_deref() {
            let fields = parse_dict(publish);
            if fields.get("private").map(String::as_str) == Some("true") {
                json.insert("private", Value::b(true));
            }
            if let Some(files_vec) = extract_vector_block(publish, ":files-include") {
                let items: Vec<Value> = files_vec
                    .split_whitespace()
                    .map(|s| Value::s(s.trim_matches('"')))
                    .filter(|v| matches!(v, Value::String(s) if !s.is_empty()))
                    .collect();
                if !items.is_empty() {
                    json.insert("files", Value::Array(items));
                }
            }
        }
        json.insert("main", Value::s("index.js"));

        let mut scripts_obj = Value::obj();
        if let Some(scripts) = c.scripts_raw.as_deref() {
            let s_fields = parse_dict(scripts);
            for (k, v) in &s_fields {
                scripts_obj.insert(k.as_str(), Value::s(v));
            }
        }
        if matches!(&scripts_obj, Value::Object(items) if items.is_empty()) {
            scripts_obj.insert("test", Value::s("echo OK"));
        }
        json.insert("scripts", scripts_obj);

        let deps = parse_deps_raw(c.dependencies_raw.as_deref().unwrap_or(""));
        emit_npm_deps_block_ast(&mut json, "dependencies", &deps.runtime);
        emit_npm_deps_block_ast(&mut json, "devDependencies", &deps.dev);

        fs::write(&pkg_json, render_json(&json))?;
        written.push(pkg_json);
    }
    render_ci_shims(c, out, force, &mut written)?;
    Ok(written)
}

fn emit_npm_deps_block_ast(
    json: &mut crate::json_ast::Value,
    key: &str,
    deps: &[BTreeMap<String, String>],
) {
    use crate::json_ast::Value;
    if deps.is_empty() { return; }
    let mut block = Value::obj();
    for d in deps {
        if let Some(name) = d.get("name") {
            let ver = d.get("version").cloned().unwrap_or_else(|| "*".to_string());
            block.insert(name.as_str(), Value::s(ver));
        }
    }
    json.insert(key, block);
}

fn emit_npm_deps_block(json: &mut String, key: &str, deps: &[BTreeMap<String, String>]) {
    if deps.is_empty() {
        return;
    }
    let entries: Vec<String> = deps.iter()
        .filter_map(|d| {
            let name = d.get("name")?;
            let ver = d.get("version").cloned().unwrap_or_else(|| "*".to_string());
            Some(format!("    \"{name}\": \"{ver}\""))
        })
        .collect();
    if !entries.is_empty() {
        json.push_str(&format!("  \"{key}\": {{\n{}\n  }},\n", entries.join(",\n")));
    }
}

fn render_python(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
    use crate::toml_ast::{Document, Value as TVal};
    let mut written = vec![];
    fs::create_dir_all(out)?;

    let pyproj = out.join("pyproject.toml");
    if !pyproj.exists() || force {
        let pkg = &c.package;
        let mut doc = Document::new();

        // [project]
        {
            let p = doc.table("project");
            if let Some(n) = pkg.get("name") { p.key("name", TVal::s(n)); }
            if let Some(v) = pkg.get("version") { p.key("version", TVal::s(v)); }
            if let Some(d) = pkg.get("description") { p.key("description", TVal::s(d)); }
            if let Some(l) = pkg.get("license") {
                // PEP 621: license = { text = "MIT" } — typed inline table.
                let mut inline = std::collections::BTreeMap::new();
                inline.insert("text".to_string(), TVal::s(l));
                p.key("license", TVal::InlineTable(inline));
            }
            if let Some(supports) = c.supports_raw.as_deref() {
                if let Some(py_req) = read_keyword_in_block(supports, ":python") {
                    let clean = py_req.trim_matches('"').to_string();
                    p.key("requires-python", TVal::s(clean));
                }
            }
            if c.supports_raw.is_none() {
                if let Some(rp) = pkg.get("requires-python") {
                    p.key("requires-python", TVal::s(rp));
                }
            }
            let deps = parse_deps_raw(c.dependencies_raw.as_deref().unwrap_or(""));
            if !deps.runtime.is_empty() {
                let items: Vec<TVal> = deps.runtime.iter()
                    .filter_map(|d| {
                        let n = d.get("name")?;
                        let v = d.get("version").cloned().unwrap_or_default();
                        Some(if v.is_empty() {
                            TVal::s(n.clone())
                        } else {
                            // PEP 508 requirement string — typed
                            // concat through String, then typed escape.
                            let mut s = n.clone();
                            s.push_str(">=");
                            s.push_str(&v);
                            TVal::s(s)
                        })
                    })
                    .collect();
                p.key("dependencies", TVal::Array(items));
            }
        }

        // [project.optional-dependencies]
        let deps = parse_deps_raw(c.dependencies_raw.as_deref().unwrap_or(""));
        if !deps.dev.is_empty() {
            let items: Vec<TVal> = deps.dev.iter()
                .filter_map(|d| {
                    let n = d.get("name")?;
                    let v = d.get("version").cloned().unwrap_or_default();
                    Some(if v.is_empty() {
                        TVal::s(n.clone())
                    } else {
                        let mut s = n.clone();
                        s.push_str(">=");
                        s.push_str(&v);
                        TVal::s(s)
                    })
                })
                .collect();
            doc.table("project.optional-dependencies").key("dev", TVal::Array(items));
        }

        // [project.scripts]
        if let Some(scripts) = c.scripts_raw.as_deref() {
            let s_fields = parse_dict(scripts);
            if !s_fields.is_empty() {
                let scripts_t = doc.table("project.scripts");
                for (k, v) in &s_fields {
                    scripts_t.key(k.clone(), TVal::s(v));
                }
            }
        }

        // [build-system]
        let bs = doc.table("build-system");
        bs.key("requires", TVal::arr([TVal::s("hatchling")]));
        bs.key("build-backend", TVal::s("hatchling.build"));

        fs::write(&pyproj, doc.render())?;
        written.push(pyproj);
    }
    render_ci_shims(c, out, force, &mut written)?;
    Ok(written)
}

fn render_helm(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
    use crate::yaml_ast::{render as render_yaml, Value};
    let mut written = vec![];
    fs::create_dir_all(out)?;
    fs::create_dir_all(out.join("templates"))?;

    let chart_yaml = out.join("Chart.yaml");
    if !chart_yaml.exists() || force {
        let pkg = &c.package;
        let mut chart = Value::map();
        chart.insert("apiVersion", Value::s("v2"));
        if let Some(n) = pkg.get("name") { chart.insert("name", Value::s(n)); }
        if let Some(d) = pkg.get("description") { chart.insert("description", Value::s(d)); }
        chart.insert("type", Value::s(pkg.get("type").map(String::as_str).unwrap_or("application")));
        if let Some(v) = pkg.get("version") { chart.insert("version", Value::s(v)); }
        if let Some(av) = pkg.get("appVersion") { chart.insert("appVersion", Value::s(av)); }
        fs::write(&chart_yaml, render_yaml(&chart))?;
        written.push(chart_yaml);
    }
    let values = out.join("values.yaml");
    if !values.exists() || force {
        fs::write(&values, "# values.yaml — fill in per the chart's templates\n")?;
        written.push(values);
    }
    render_ci_shims(c, out, force, &mut written)?;
    Ok(written)
}

fn render_rust_workspace(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
    use crate::toml_ast::{Document, Value as TVal};
    let mut written = vec![];
    fs::create_dir_all(out)?;

    let cargo = out.join("Cargo.toml");
    if !cargo.exists() || force {
        let ws = &c.package;
        let mut doc = Document::new();

        // [workspace]
        {
            let w = doc.table("workspace");
            if let Some(members) = c.package_lists.get("members") {
                w.key("members", TVal::arr(members.iter().map(TVal::s)));
            }
            w.key("resolver", TVal::s("2"));
        }

        // [workspace.package]
        {
            let p = doc.table("workspace.package");
            if let Some(v) = ws.get("version") { p.key("version", TVal::s(v)); }
            p.key("edition", TVal::s("2024"));
            if let Some(l) = ws.get("license") { p.key("license", TVal::s(l)); }
            if let Some(r) = ws.get("repository") { p.key("repository", TVal::s(r)); }
        }

        fs::write(&cargo, doc.render())?;
        written.push(cargo);
    }
    render_ci_shims(c, out, force, &mut written)?;
    Ok(written)
}

fn render_github_action(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
    let mut written = vec![];
    fs::create_dir_all(out)?;

    let action_yml = out.join("action.yml");
    if !action_yml.exists() || force {
        let pkg = &c.package;
        let mut yml = format!("name: 'pleme-io · {}'\n", c.name);
        if let Some(d) = pkg.get("description") { yml.push_str(&format!("description: '{d}'\n")); }
        yml.push_str("branding: { icon: 'box', color: 'green' }\n\n");
        yml.push_str("inputs: {}\noutputs: {}\n\n");
        yml.push_str("runs:\n  using: composite\n  steps:\n");
        yml.push_str("    - id: src\n      shell: bash\n      run: |\n");
        yml.push_str("        {\n          echo 'script<<TLISP_EOF'\n");
        yml.push_str("          curl -sL https://raw.githubusercontent.com/pleme-io/actions/main/_tlisp-stdlib/stdlib.tlisp\n");
        yml.push_str("          echo\n          cat ${{ github.action_path }}/run.tlisp\n");
        yml.push_str("          echo 'TLISP_EOF'\n        } >> \"$GITHUB_OUTPUT\"\n");
        yml.push_str("    - id: run\n      uses: pleme-io/actions/tatara-script@v1\n");
        yml.push_str("      with:\n        script: ${{ steps.src.outputs.script }}\n");
        fs::write(&action_yml, yml)?;
        written.push(action_yml);
    }
    let run_tlisp = out.join("run.tlisp");
    if !run_tlisp.exists() || force {
        fs::write(&run_tlisp, ";; run.tlisp — rendered from .caixa.lisp\n(log-info \"::warning::stub body\")\n(exit 0)\n")?;
        written.push(run_tlisp);
    }
    render_ci_shims(c, out, force, &mut written)?;
    Ok(written)
}

const AUTO_RELEASE_SHIM: &str = r#"name: auto-release
# Generated from caixa source. Edit caixa, re-render.

on:
  push:
    branches: [main]
  workflow_dispatch:
    inputs:
      bump-type:
        description: "patch | minor | major"
        default: patch

jobs:
  release:
    uses: pleme-io/substrate/.github/workflows/auto-release.yml@main
    with:
      bump-type: ${{ inputs.bump-type || 'patch' }}
    secrets: inherit
"#;

const PRE_MERGE_GATE_SHIM: &str = r#"name: pre-merge-gate
# Generated from caixa source. Edit caixa, re-render.

on:
  pull_request:
    branches: [main]

jobs:
  gate:
    uses: pleme-io/substrate/.github/workflows/pre-merge-gate.yml@main
    secrets: inherit
"#;

const SECURITY_GATE_SHIM: &str = r#"name: security-gate
# Generated from caixa source. Edit caixa, re-render.

on:
  pull_request:
    branches: [main]
  schedule:
    - cron: '0 6 * * 1'

jobs:
  gate:
    uses: pleme-io/substrate/.github/workflows/security-gate.yml@main
    secrets: inherit
"#;

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

    #[test]
    fn rust_library_caixa_renders() {
        let src = r#"
(defcaixa my-lib
  :kind :Biblioteca
  :ecosystem :rust-single-crate
  :package { :name "my-lib" :version "0.1.0" :description "A test lib." :license "MIT" }
  :ci-config { :bump { :default-type "patch" :skip-when-no-source-changes true } }
  :workflows [ :auto-release :pre-merge-gate :security-gate ])
"#;
        let parsed = parse(src).unwrap();
        assert_eq!(parsed.name, "my-lib");
        assert_eq!(parsed.kind, "Biblioteca");
        assert_eq!(parsed.ecosystem, "rust-single-crate");
        assert_eq!(parsed.package.get("name").map(String::as_str), Some("my-lib"));
        assert_eq!(parsed.package.get("version").map(String::as_str), Some("0.1.0"));
        assert_eq!(parsed.workflows, vec!["auto-release", "pre-merge-gate", "security-gate"]);
        assert!(parsed.ci_config.contains_key("bump"));
    }
}
