//! Generation of `src/admins/admin.rs` — `DynForm` wrappers and `admin_register()` from `ResourceDef`.
use crate::admin::daemon::parser::{ParsedAdmin, ResourceDef};
use std::{fmt::Write, fs, path::Path};

/// Generates `src/admins/admin.rs` — the only file produced by the daemon.
///
/// Contains:
/// - One `DynForm` implementation per resource (wrapping the concrete `RuniqueForm`)
/// - `admin_register()` → `HashMap<String, ResourceEntry>` populated at boot
pub(crate) fn generate(parsed: &ParsedAdmin) -> Result<(), String> {
    let admins_dir = Path::new("src/admins");
    if admins_dir.exists() {
        fs::remove_dir_all(admins_dir)
            .map_err(|e| format!("Impossible to delete {}: {}", admins_dir.display(), e))?;
    }
    fs::create_dir_all(admins_dir)
        .map_err(|e| format!("Impossible to create {}: {}", admins_dir.display(), e))?;

    write_readme(admins_dir)?;
    write_admin(parsed, admins_dir)?;
    write_mod(admins_dir)?;

    Ok(())
}

fn write_readme(dir: &Path) -> Result<(), String> {
    let content = "<!-- AUTO-admin — DO NOT EDIT MANUALLY\n     admin by `runique start`. Any changes will be overwritten. -->\n";
    fs::write(dir.join("README.md"), content)
        .map_err(|e| format!("Impossible to write README.md: {}", e))
}

fn write_mod(dir: &Path) -> Result<(), String> {
    let content = "pub mod admin;\npub use admin::{routes, admin_state};\n";
    fs::write(dir.join("mod.rs"), content).map_err(|e| format!("Impossible to write mod.rs: {}", e))
}

fn write_admin(parsed: &ParsedAdmin, dir: &Path) -> Result<(), String> {
    let resources = &parsed.resources;
    let mut out = String::new();

    let _ = writeln!(out, "// AUTO-admin — DO NOT EDIT MANUALLY");
    let _ = writeln!(out, "// admin by `runique start` from src/admin.rs");
    let _ = writeln!(out);
    let _ = writeln!(out, "use runique::prelude::*;");
    let _ = writeln!(out);

    // Entity imports (AdminForm is generated by model! in each module)
    for r in resources {
        let module = model_to_module(&r.model_type);
        let _ = writeln!(out, "use crate::entities::{};", module);
    }
    let _ = writeln!(out);

    // One DynForm implementation per resource (main form + edit_form if declared)
    for r in resources {
        write_dyn_form_impl(&mut out, r)?;
        if r.edit_form_type.is_some() {
            write_edit_dyn_form_impl(&mut out, r)?;
        } else if r.bulk_create.is_some() {
            // bulk_create without explicit edit_form: auto-generate edit wrapper using AdminForm
            let module = model_to_module(&r.model_type);
            let default_form = format!("{}::AdminForm", module);
            let edit_wrapper = format!("{}EditFormDynWrapper", pascal_case(&module));
            write_dyn_form_wrapper(
                &mut out,
                &format!(
                    "DynForm edit wrapper for {} (auto from bulk_create)",
                    module
                ),
                &edit_wrapper,
                &default_form,
                "_db",
                "Ok(()) // update_fn handles persistence",
            )?;
        }
    }

    // admin_register()
    write_admin_register(&mut out, parsed)?;

    fs::write(dir.join("admin.rs"), out).map_err(|e| format!("Impossible to write admin.rs: {}", e))
}

fn write_dyn_form_impl(out: &mut String, r: &ResourceDef) -> Result<(), String> {
    let module = model_to_module(&r.model_type);
    let form_path = r
        .create_form_type
        .as_deref()
        .unwrap_or(&format!("{}::AdminForm", module))
        .to_string();
    let wrapper = format!("{}AdminFormDynWrapper", pascal_case(&module));
    write_dyn_form_wrapper(
        out,
        &format!("DynForm wrapper for {}::AdminForm", module),
        &wrapper,
        &form_path,
        "db",
        "self.0.save(db).await",
    )
}

fn write_edit_dyn_form_impl(out: &mut String, r: &ResourceDef) -> Result<(), String> {
    let edit_form_path = r.edit_form_type.as_ref().unwrap();
    let module = model_to_module(&r.model_type);
    let wrapper = format!("{}EditFormDynWrapper", pascal_case(&module));
    write_dyn_form_wrapper(
        out,
        &format!("DynForm edit wrapper for {}", edit_form_path),
        &wrapper,
        edit_form_path,
        "_db",
        "Ok(()) // update_fn handles persistence",
    )
}

fn write_admin_register(out: &mut String, parsed: &ParsedAdmin) -> Result<(), String> {
    let resources = &parsed.resources;

    let _ = writeln!(out, "/// Builds the admin registry at boot.");
    let _ = writeln!(out, "/// Called by the application builder.");
    let _ = writeln!(out, "pub fn admin_register() -> AdminRegistry {{");
    let _ = writeln!(out, "    let mut registry = AdminRegistry::new();");
    let _ = writeln!(
        out,
        "    for entry in runique::admin::builtin_resources() {{"
    );
    let _ = writeln!(out, "        registry.register(entry);");
    let _ = writeln!(out, "    }}");
    let _ = writeln!(out);

    for r in resources {
        write_resource_entry(out, r)?;
    }

    // Applies configure{}: overrides the DisplayConfig and group_actions of any resource
    for cfg in &parsed.configures {
        if cfg.hidden {
            let _ = writeln!(out, "    registry.remove(\"{}\");", cfg.key);
            continue;
        }
        let has_display = !cfg.list_display.is_empty()
            || !cfg.list_exclude.is_empty()
            || !cfg.list_filter.is_empty();
        if has_display {
            let display_3: Vec<(
                String,
                String,
                Option<crate::admin::daemon::parser::FkDisplay>,
            )> = cfg
                .list_display
                .iter()
                .map(|(c, l)| (c.clone(), l.clone(), None))
                .collect();
            let chain = build_display_chain(&display_3, &cfg.list_exclude, &cfg.list_filter);
            let _ = writeln!(out, "    registry.configure(\"{}\", {});", cfg.key, chain);
        }
        if !cfg.group_action.is_empty() {
            let actions_str = build_group_actions_str(&cfg.group_action);
            let _ = writeln!(
                out,
                "    registry.configure_group_actions(\"{}\", vec![{}]);",
                cfg.key, actions_str
            );
        }
    }

    let _ = writeln!(out, "    registry");
    let _ = writeln!(out, "}}");
    let _ = writeln!(out);

    // routes() function — returns the Axum Router of the admin prototype
    let _ = writeln!(
        out,
        "/// Builds the admin CRUD routes for the given prefix."
    );
    let _ = writeln!(
        out,
        "/// To be passed to `.with_admin(|a| a.routes(admins::routes(\"/admin\")))` in main.rs."
    );
    let _ = writeln!(
        out,
        "/// The prefix is automatically propagated to `AdminConfig` — no need to call `.prefix()` separately."
    );
    let _ = writeln!(
        out,
        "pub fn routes(prefix: &str) -> runique::admin::AdminRoutes {{"
    );
    let _ = writeln!(out, "    let p = prefix.trim_end_matches('/');");
    let _ = writeln!(out, "    let router = runique::axum::Router::new()");
    let _ = writeln!(
        out,
        "        .route(&format!(\"{{}}/{{{{resource}}}}/{{{{action}}}}\", p), get(admin_get).post(admin_post))"
    );
    let _ = writeln!(
        out,
        "        .route(&format!(\"{{}}/{{{{resource}}}}/{{{{id}}}}/{{{{action}}}}\", p), get(admin_get_id).post(admin_post_id));"
    );
    let _ = writeln!(out, "    runique::admin::AdminRoutes::new(p, router)");
    let _ = writeln!(out, "}}");
    let _ = writeln!(out);
    let _ = writeln!(
        out,
        "/// Returns the shared state of the admin prototype (for the dashboard)."
    );
    let _ = writeln!(
        out,
        "pub fn admin_state() -> std::sync::Arc<PrototypeAdminState> {{"
    );
    let _ = writeln!(out, "    let config = Arc::new(AdminConfig::new());");
    let _ = writeln!(out, "    std::sync::Arc::new(PrototypeAdminState {{");
    let _ = writeln!(out, "        registry: Arc::new(admin_register()),");
    let _ = writeln!(out, "        config,");
    let _ = writeln!(out, "    }})");
    let _ = writeln!(out, "}}");

    Ok(())
}

fn write_resource_entry(out: &mut String, r: &ResourceDef) -> Result<(), String> {
    let key = &r.key;
    let model = full_model_path(&r.model_type);
    let title = &r.title;
    let module = model_to_module(&r.model_type);
    let form_path = r
        .create_form_type
        .as_deref()
        .unwrap_or(&format!("{}::AdminForm", module))
        .to_string();
    let wrapper = format!("{}AdminFormDynWrapper", pascal_case(&module));
    // ID conversion code from String according to id_type
    let id_parse_code = match r.id_type.as_str() {
        "I32" => {
            "let id = id.parse::<i32>().map_err(|_| DbErr::Custom(\"invalid id\".to_string()))?"
        }
        "I64" => {
            "let id = id.parse::<i64>().map_err(|_| DbErr::Custom(\"invalid id\".to_string()))?"
        }
        "Uuid" => {
            "let id = uuid::Uuid::parse_str(&id).map_err(|_| DbErr::Custom(\"invalid id\".to_string()))?"
        }
        _ => {
            // default "Pk": follows the big-pk feature flag automatically
            "let id = id.parse::<Pk>().map_err(|_| DbErr::Custom(\"invalid id\".to_string()))?"
        }
    };

    // AdminResource
    let _ = writeln!(out, "    // ── Resource: {} ──", key);
    let _ = writeln!(out, "    let meta = AdminResource::new(");
    let _ = writeln!(out, "        \"{}\",", key);
    let _ = writeln!(out, "        \"{}\",", model);
    let _ = writeln!(out, "        \"AdminForm\",");
    let _ = writeln!(out, "        \"{}\",", title);
    let _ = writeln!(out, "        vec![],");
    let _ = writeln!(out, "    );");

    // inject_password: enabled when a custom create_form is declared
    if r.create_form_type.is_some() {
        let _ = writeln!(out, "    let meta = meta.inject_password(true);");
    }

    // Template overrides
    if let Some(ref t) = r.template_list {
        let _ = writeln!(out, "    let meta = meta.template_list(\"{}\");", t);
    }
    if let Some(ref t) = r.template_create {
        let _ = writeln!(out, "    let meta = meta.template_create(\"{}\");", t);
    }
    if let Some(ref t) = r.template_edit {
        let _ = writeln!(out, "    let meta = meta.template_edit(\"{}\");", t);
    }
    if let Some(ref t) = r.template_detail {
        let _ = writeln!(out, "    let meta = meta.template_detail(\"{}\");", t);
    }
    if let Some(ref t) = r.template_delete {
        let _ = writeln!(out, "    let meta = meta.template_delete(\"{}\");", t);
    }

    // Extra context
    for (k, v) in &r.extra_context {
        let _ = writeln!(out, "    let meta = meta.extra(\"{}\", \"{}\");", k, v);
    }

    // FormBuilder closure — injecte les ChoiceFields FK si déclarés dans list_display
    let fk_cols_for_form: Vec<_> = r
        .list_display
        .iter()
        .filter(|(_, _, fk)| fk.is_some())
        .collect();
    write_form_builder_closure_fk(out, "form_builder", &form_path, &wrapper, &fk_cols_for_form);

    // ListFn closure
    let _ = writeln!(
        out,
        "    let list_fn: ListFn = Arc::new(|db: ADb, params: ListParams| {{"
    );
    let _ = writeln!(out, "        Box::pin(async move {{");
    let _ = writeln!(
        out,
        "            use sea_orm::{{QueryFilter, sea_query::{{Alias, Expr, Order}}}};"
    );
    let _ = writeln!(
        out,
        "            let mut query = {}::Entity::find();",
        module
    );

    // Whitelist columns for sort and filters — built at code generation time from DSL declarations.
    // Any col not in the whitelist is silently ignored, preventing SQL injection via URL parameters.
    let sort_cols: Vec<String> = std::iter::once("id".to_string())
        .chain(r.list_display.iter().map(|(col, _, _)| col.clone()))
        .collect();
    let sort_cols_literal = sort_cols
        .iter()
        .map(|c| format!("\"{}\"", c))
        .collect::<Vec<_>>()
        .join(", ");
    let filter_cols: Vec<String> = r
        .list_filter
        .iter()
        .map(|(col, _, _)| col.clone())
        .collect();
    let filter_cols_literal = filter_cols
        .iter()
        .map(|c| format!("\"{}\"", c))
        .collect::<Vec<_>>()
        .join(", ");
    let _ = writeln!(
        out,
        "            const SORT_COLS: &[&str] = &[{sort_cols_literal}];",
        sort_cols_literal = sort_cols_literal
    );
    let _ = writeln!(
        out,
        "            const FILTER_COLS: &[&str] = &[{filter_cols_literal}];",
        filter_cols_literal = filter_cols_literal
    );

    let _ = writeln!(out, "            if let Some(ref col) = params.sort_by {{");
    let _ = writeln!(
        out,
        "                if SORT_COLS.contains(&col.as_str()) {{"
    );
    let _ = writeln!(
        out,
        "                    let order = if params.sort_dir == SortDir::Desc {{ Order::Desc }} else {{ Order::Asc }};"
    );
    let _ = writeln!(
        out,
        "                    query = query.order_by(Expr::col(Alias::new(col.as_str())), order);"
    );
    let _ = writeln!(out, "                }}");
    let _ = writeln!(out, "            }}");
    let _ = writeln!(
        out,
        "            for (col, val) in &params.column_filters {{"
    );
    let _ = writeln!(
        out,
        "                if !FILTER_COLS.contains(&col.as_str()) {{ continue; }}"
    );
    let _ = writeln!(
        out,
        "                let escaped = val.replace('\\'', \"''\");"
    );
    let _ = writeln!(
        out,
        "                query = query.filter(Expr::cust(format!(\"CAST({{}} AS TEXT) = '{{}}'\", col, escaped)));"
    );
    let _ = writeln!(out, "            }}");
    let _ = writeln!(
        out,
        "            if let Some(ref search_str) = params.search {{"
    );
    write_search_conditions(out, &r.list_display, &module);
    let _ = writeln!(out, "                query = query.filter(search_cond);");
    let _ = writeln!(out, "            }}");
    let _ = writeln!(
        out,
        "            let db_rows = query.offset(params.offset).limit(params.limit).all(&*db).await?;"
    );
    let has_fk = r.list_display.iter().any(|(_, _, fk)| fk.is_some());
    let rows_binding = if has_fk { "let mut rows" } else { "let rows" };
    let _ = writeln!(
        out,
        "            {rows_binding}: Vec<serde_json::Value> = db_rows.into_iter()",
        rows_binding = rows_binding
    );
    let _ = writeln!(
        out,
        "                .map(|r| serde_json::to_value(r).unwrap_or(serde_json::Value::Null))"
    );
    let _ = writeln!(out, "                .collect();");
    // FK resolution blocks — fonctionne pour i32, i64 et UUID (clé String unifiée)
    let fk_cols: Vec<_> = r
        .list_display
        .iter()
        .filter(|(_, _, fk)| fk.is_some())
        .collect();
    for (col, _, fk_opt) in &fk_cols {
        let fk = fk_opt.as_ref().unwrap();
        let safe_var = col.replace('.', "_");
        let _ = writeln!(out, "            {{");
        let _ = writeln!(out, "                use sea_orm::ConnectionTrait;");
        // Extrait l'ID comme String que ce soit un entier ou un UUID
        let _ = writeln!(
            out,
            "                let fk_ids: Vec<String> = rows.iter().filter_map(|r| r.get(\"{col}\").and_then(|v| v.as_i64().map(|n| n.to_string()).or_else(|| v.as_str().map(str::to_string)))).collect::<std::collections::HashSet<String>>().into_iter().collect();",
            col = col
        );
        let _ = writeln!(out, "                if !fk_ids.is_empty() {{");
        // Quote chaque valeur → fonctionne pour entiers et UUIDs
        let _ = writeln!(
            out,
            "                    let ids_csv = fk_ids.iter().map(|s| format!(\"'{{}}'\", s.replace('\\'', \"''\"))).collect::<Vec<_>>().join(\",\");"
        );
        // CAST(id AS TEXT) → compatible i32, i64, UUID
        let _ = writeln!(
            out,
            "                    let _fk_stmt_{safe} = sea_orm::sea_query::Query::select().expr(sea_orm::sea_query::Expr::cust(\"CAST(id AS TEXT)\")).expr(sea_orm::sea_query::Expr::cust(\"{fk_col}\")).from(sea_orm::sea_query::Alias::new(\"{fk_table}\")).and_where(sea_orm::sea_query::Expr::cust(format!(\"CAST(id AS TEXT) IN ({{}})\", ids_csv))).to_owned();",
            safe = safe_var,
            fk_col = fk.col,
            fk_table = fk.table,
        );
        let _ = writeln!(
            out,
            "                    let label_map_{safe}: std::collections::HashMap<String, String> = db.query_all(&_fk_stmt_{safe}).await.unwrap_or_default().iter().filter_map(|row| {{ let id = row.try_get_by_index::<String>(0).ok()?; let label = row.try_get_by_index::<String>(1).ok()?; Some((id, label)) }}).collect();",
            safe = safe_var,
        );
        let _ = writeln!(out, "                    for row in &mut rows {{");
        // Extrait la clé String pour le lookup (même logique que pour fk_ids)
        let _ = writeln!(
            out,
            "                        if let Some(key) = row.get(\"{col}\").and_then(|v| v.as_i64().map(|n| n.to_string()).or_else(|| v.as_str().map(str::to_string)))",
            col = col
        );
        let _ = writeln!(
            out,
            "                            && let Some(label) = label_map_{safe}.get(&key) {{",
            safe = safe_var
        );
        let _ = writeln!(
            out,
            "                            row[\"{col}\"] = serde_json::Value::String(label.clone());",
            col = col
        );
        let _ = writeln!(out, "                        }}");
        let _ = writeln!(out, "                    }}");
        let _ = writeln!(out, "                }}");
        let _ = writeln!(out, "            }}");
    }
    let _ = writeln!(out, "            Ok(rows)");
    let _ = writeln!(out, "        }})");
    let _ = writeln!(out, "    }});");
    let _ = writeln!(out);

    // CountFn closure
    let _ = writeln!(
        out,
        "    let count_fn: CountFn = Arc::new(|db: ADb, _search: Option<String>| {{"
    );
    let _ = writeln!(out, "        Box::pin(async move {{");
    let _ = writeln!(out, "            use sea_orm::QueryFilter;");
    let _ = writeln!(
        out,
        "            let mut query = {}::Entity::find();",
        module
    );
    let _ = writeln!(out, "            if let Some(ref search_str) = _search {{");
    write_search_conditions(out, &r.list_display, &module);
    let _ = writeln!(out, "                query = query.filter(search_cond);");
    let _ = writeln!(out, "            }}");
    let _ = writeln!(out, "            query.count(&*db).await");
    let _ = writeln!(out, "        }})");
    let _ = writeln!(out, "    }});");
    let _ = writeln!(out);

    // GetFn closure
    let _ = writeln!(
        out,
        "    let get_fn: GetFn = Arc::new(|db: ADb, id: String| {{"
    );
    let _ = writeln!(out, "        Box::pin(async move {{");
    let _ = writeln!(out, "            {};", id_parse_code);
    let _ = writeln!(
        out,
        "            let row = {}::Entity::find_by_id(id).one(&*db).await?;",
        module
    );
    if fk_cols.is_empty() {
        let _ = writeln!(
            out,
            "            Ok(row.map(|r| serde_json::to_value(r).unwrap_or(serde_json::Value::Null)))"
        );
    } else {
        let _ = writeln!(
            out,
            "            let Some(model) = row else {{ return Ok(None); }};"
        );
        let _ = writeln!(
            out,
            "            let mut row = serde_json::to_value(model).unwrap_or(serde_json::Value::Null);"
        );
        for (col, _, fk_opt) in &fk_cols {
            let fk = fk_opt.as_ref().unwrap();
            let safe_var = col.replace('.', "_");
            let _ = writeln!(out, "            {{");
            let _ = writeln!(out, "                use sea_orm::ConnectionTrait;");
            let _ = writeln!(
                out,
                "                if let Some(fk_key) = row.get(\"{col}\").and_then(|v| v.as_i64().map(|n| n.to_string()).or_else(|| v.as_str().map(str::to_string))) {{",
                col = col
            );
            let _ = writeln!(
                out,
                "                    let ids_csv = format!(\"'{{}}'\", fk_key.replace('\\'', \"''\"));"
            );
            let _ = writeln!(
                out,
                "                    let _fk_stmt_{safe} = sea_orm::sea_query::Query::select().expr(sea_orm::sea_query::Expr::cust(\"CAST(id AS TEXT)\")).expr(sea_orm::sea_query::Expr::cust(\"{fk_col}\")).from(sea_orm::sea_query::Alias::new(\"{fk_table}\")).and_where(sea_orm::sea_query::Expr::cust(format!(\"CAST(id AS TEXT) IN ({{}})\", ids_csv))).to_owned();",
                safe = safe_var,
                fk_col = fk.col,
                fk_table = fk.table,
            );
            let _ = writeln!(
                out,
                "                    if let Some(fk_row) = db.query_one(&_fk_stmt_{safe}).await.ok().flatten()",
                safe = safe_var,
            );
            let _ = writeln!(
                out,
                "                        && let Ok(label) = fk_row.try_get_by_index::<String>(1) {{"
            );
            let _ = writeln!(
                out,
                "                        row[\"{col}\"] = serde_json::Value::String(label);",
                col = col
            );
            let _ = writeln!(out, "                    }}");
            let _ = writeln!(out, "                }}");
            let _ = writeln!(out, "            }}");
        }
        let _ = writeln!(out, "            Ok(Some(row))");
    }
    let _ = writeln!(out, "        }})");
    let _ = writeln!(out, "    }});");
    let _ = writeln!(out);

    // DeleteFn closure
    let _ = writeln!(
        out,
        "    let delete_fn: DeleteFn = Arc::new(|db: ADb, id: String| {{"
    );
    let _ = writeln!(out, "        Box::pin(async move {{");
    let _ = writeln!(out, "            {};", id_parse_code);
    let _ = writeln!(
        out,
        "            {}::Entity::delete_by_id(id).exec(&*db).await.map(|_| ())",
        module
    );
    let _ = writeln!(out, "        }})");
    let _ = writeln!(out, "    }});");
    let _ = writeln!(out);

    // CreateFn closure — uses admin_from_form() generated by the proc-macro
    let _ = writeln!(
        out,
        "    let create_fn: CreateFn = Arc::new(|db: ADb, data: StrMap| {{"
    );
    let _ = writeln!(out, "        Box::pin(async move {{");
    if let Some(ref bulk_field) = r.bulk_create {
        // Bulk upsert: split the specified field by comma, update if exists, insert otherwise
        let _ = writeln!(out, "            use sea_orm::QueryFilter;");
        let _ = writeln!(
            out,
            "            let raw = data.get(\"{bulk_field}\").cloned().unwrap_or_default();",
            bulk_field = bulk_field
        );
        let _ = writeln!(
            out,
            "            let values: Vec<&str> = raw.split(',').map(str::trim).filter(|v| !v.is_empty()).collect();"
        );
        let _ = writeln!(out, "            for val in values {{");
        let _ = writeln!(out, "                let mut row = data.clone();");
        let _ = writeln!(
            out,
            "                row.insert(\"{bulk_field}\".to_string(), val.to_string());",
            bulk_field = bulk_field
        );
        let _ = writeln!(
            out,
            "                let escaped_bv = val.replace('\\'', \"''\");"
        );
        let _ = writeln!(
            out,
            "                let existing_id: Option<String> = {}::Entity::find()",
            module
        );
        let _ = writeln!(
            out,
            "                    .filter(sea_orm::sea_query::Expr::cust(format!(\"CAST({bulk_field} AS TEXT) = '{{}}'\", escaped_bv)))",
            bulk_field = bulk_field
        );
        let _ = writeln!(out, "                    .one(&*db).await?");
        let _ = writeln!(out, "                    .map(|m| m.id.to_string());");
        let _ = writeln!(out, "                if let Some(id) = existing_id {{");
        let _ = writeln!(out, "                    {};", id_parse_code);
        let _ = writeln!(
            out,
            "                    {}::admin_from_form(&row, Some(id)).update(&*db).await?;",
            module
        );
        let _ = writeln!(out, "                }} else {{");
        let _ = writeln!(
            out,
            "                    {}::admin_from_form(&row, None).insert(&*db).await?;",
            module
        );
        let _ = writeln!(out, "                }}");
        let _ = writeln!(out, "            }}");
        let _ = writeln!(out, "            Ok(())");
    } else if r.m2m.is_empty() {
        let _ = writeln!(out, "            {}::admin_from_form(&data, None)", module);
        let _ = writeln!(out, "                .insert(&*db).await.map(|_| ())");
    } else {
        // With M2M: capture inserted ID, then populate junction tables
        let _ = writeln!(out, "            use sea_orm::ConnectionTrait;");
        let _ = writeln!(
            out,
            "            let result = {}::admin_from_form(&data, None).insert(&*db).await?;",
            module
        );
        let _ = writeln!(out, "            let inserted_id = result.id.to_string();");
        for m2m in &r.m2m {
            let prefix = format!("m2m_{}__", m2m.field_name);
            let _ = writeln!(out, "            for key in data.keys() {{");
            let _ = writeln!(
                out,
                "                if let Some(target_id) = key.strip_prefix(\"{prefix}\") && !target_id.is_empty() && target_id.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') {{",
                prefix = prefix
            );
            let _ = writeln!(
                out,
                "                    let sql = format!(\"INSERT INTO {junction} ({self_fk}, {target_fk}) VALUES ({{}}, {{}}) ON CONFLICT DO NOTHING\", inserted_id, target_id);",
                junction = m2m.junction_table,
                self_fk = m2m.self_fk,
                target_fk = m2m.target_fk
            );
            let _ = writeln!(
                out,
                "                    let _ = db.execute_unprepared(&sql).await;"
            );
            let _ = writeln!(out, "                }}");
            let _ = writeln!(out, "            }}");
        }
        let _ = writeln!(out, "            Ok(())");
    }
    let _ = writeln!(out, "        }})");
    let _ = writeln!(out, "    }});");
    let _ = writeln!(out);

    // UpdateFn closure — uses admin_from_form() generated by the proc-macro
    let _ = writeln!(
        out,
        "    let update_fn: UpdateFn = Arc::new(|db: ADb, id: String, data: StrMap| {{"
    );
    let _ = writeln!(out, "        Box::pin(async move {{");
    let _ = writeln!(out, "            {};", id_parse_code);
    if r.m2m.is_empty() {
        let _ = writeln!(
            out,
            "            {}::admin_from_form(&data, Some(id))",
            module
        );
        let _ = writeln!(out, "                .update(&*db).await.map(|_| ())");
    } else {
        let _ = writeln!(out, "            use sea_orm::ConnectionTrait;");
        let _ = writeln!(out, "            let id_str = id.to_string();");
        let _ = writeln!(
            out,
            "            {}::admin_from_form(&data, Some(id)).update(&*db).await?;",
            module
        );
        for m2m in &r.m2m {
            let prefix = format!("m2m_{}__", m2m.field_name);
            let _ = writeln!(
                out,
                "            let _ = db.execute_unprepared(&format!(\"DELETE FROM {junction} WHERE {self_fk} = {{}}\", id_str)).await;",
                junction = m2m.junction_table,
                self_fk = m2m.self_fk
            );
            let _ = writeln!(out, "            for key in data.keys() {{");
            let _ = writeln!(
                out,
                "                if let Some(target_id) = key.strip_prefix(\"{prefix}\") && !target_id.is_empty() && target_id.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') {{",
                prefix = prefix
            );
            let _ = writeln!(
                out,
                "                    let sql = format!(\"INSERT INTO {junction} ({self_fk}, {target_fk}) VALUES ({{}}, {{}})\", id_str, target_id);",
                junction = m2m.junction_table,
                self_fk = m2m.self_fk,
                target_fk = m2m.target_fk
            );
            let _ = writeln!(
                out,
                "                    let _ = db.execute_unprepared(&sql).await;"
            );
            let _ = writeln!(out, "                }}");
            let _ = writeln!(out, "            }}");
        }
        let _ = writeln!(out, "            Ok(())");
    }
    let _ = writeln!(out, "        }})");
    let _ = writeln!(out, "    }});");
    let _ = writeln!(out);

    // PartialUpdateFn closure — uses admin_partial_update(): NotSet for absent fields
    let _ = writeln!(
        out,
        "    let partial_update_fn: UpdateFn = Arc::new(|db: ADb, id: String, data: StrMap| {{"
    );
    let _ = writeln!(out, "        Box::pin(async move {{");
    let _ = writeln!(out, "            {};", id_parse_code);
    let _ = writeln!(
        out,
        "            {}::admin_partial_update(&data, id)",
        module
    );
    let _ = writeln!(out, "                .update(&*db).await.map(|_| ())");
    let _ = writeln!(out, "        }})");
    let _ = writeln!(out, "    }});");
    let _ = writeln!(out);

    // M2mLoaderFn closure (optional — only when m2m is declared)
    if !r.m2m.is_empty() {
        let _ = writeln!(
            out,
            "    let m2m_loader: M2mLoaderFn = Arc::new(|db: ADb, object_id: Option<String>| {{"
        );
        let _ = writeln!(out, "        Box::pin(async move {{");
        let _ = writeln!(
            out,
            "            use sea_orm::{{EntityTrait, ConnectionTrait}};"
        );
        let _ = writeln!(
            out,
            "            let mut fields: Vec<M2mFieldOptions> = Vec::new();"
        );
        for m2m in &r.m2m {
            let _ = writeln!(out, "            {{");
            let _ = writeln!(
                out,
                "                let rows = {entity}::Entity::find().all(&*db).await.unwrap_or_default();",
                entity = m2m.target_entity
            );
            let _ = writeln!(
                out,
                "                let choices: Vec<(String, String)> = rows.iter().map(|r| {{"
            );
            let _ = writeln!(
                out,
                "                    let v = serde_json::to_value(r).unwrap_or_default();"
            );
            let _ = writeln!(
                out,
                "                    let id = v.get(\"id\").map(|i| i.to_string().trim_matches('\"').to_string()).unwrap_or_default();"
            );
            let _ = writeln!(
                out,
                "                    let label = v.get(\"{display}\").and_then(|n| n.as_str()).unwrap_or(\"\").to_string();",
                display = m2m.target_display
            );
            let _ = writeln!(out, "                    (id, label)");
            let _ = writeln!(out, "                }}).collect();");
            let _ = writeln!(
                out,
                "                let selected = if let Some(ref oid) = object_id {{"
            );
            let _ = writeln!(
                out,
                "                    use sea_orm::sea_query::{{Query, Alias, Expr}};"
            );
            let _ = writeln!(
                out,
                "                    let stmt = Query::select().expr(Expr::cust(\"CAST({target_fk} AS TEXT)\")).from(Alias::new(\"{junction}\")).and_where(Expr::cust(format!(\"CAST({self_fk} AS TEXT) = '{{}}'\", oid))).to_owned();",
                target_fk = m2m.target_fk,
                junction = m2m.junction_table,
                self_fk = m2m.self_fk
            );
            let _ = writeln!(
                out,
                "                    db.query_all(&stmt).await.unwrap_or_default().iter().filter_map(|r| r.try_get_by_index::<String>(0).ok()).collect()"
            );
            let _ = writeln!(out, "                }} else {{ vec![] }};");
            let _ = writeln!(out, "                fields.push(M2mFieldOptions {{");
            let _ = writeln!(
                out,
                "                    field_name: \"{field_name}\".to_string(),",
                field_name = m2m.field_name
            );
            let _ = writeln!(
                out,
                "                    label: \"{label}\".to_string(),",
                label = m2m.label
            );
            let _ = writeln!(out, "                    choices,");
            let _ = writeln!(out, "                    selected,");
            let _ = writeln!(out, "                }});");
            let _ = writeln!(out, "            }}");
        }
        let _ = writeln!(out, "            fields");
        let _ = writeln!(out, "        }})");
        let _ = writeln!(out, "    }});");
        let _ = writeln!(out);
    }

    // EditFormBuilder closure (optional, or auto when bulk_create is declared)
    if let Some(ref edit_form_path) = r.edit_form_type {
        let edit_wrapper = format!("{}EditFormDynWrapper", pascal_case(&module));
        write_form_builder_closure(out, "edit_form_builder", edit_form_path, &edit_wrapper);
    } else if r.bulk_create.is_some() {
        let default_form = format!("{}::AdminForm", module);
        let edit_wrapper = format!("{}EditFormDynWrapper", pascal_case(&module));
        write_form_builder_closure(out, "edit_form_builder", &default_form, &edit_wrapper);
    }

    // DisplayConfig with list_display, list_exclude and/or list_filter if configured
    if !r.list_display.is_empty() || !r.list_exclude.is_empty() || !r.list_filter.is_empty() {
        let chain = build_display_chain(&r.list_display, &r.list_exclude, &r.list_filter);
        let _ = writeln!(out, "    let meta = meta.display({});", chain);
    }

    // FilterFn closure (distinct values for each configured column)
    if !r.list_filter.is_empty() {
        let _ = writeln!(
            out,
            "    let filter_fn: FilterFn = Arc::new(|db: ADb, pages: std::collections::HashMap<String, u64>| {{"
        );
        let _ = writeln!(out, "        Box::pin(async move {{");
        let _ = writeln!(
            out,
            "            use sea_orm::{{ConnectionTrait, ExprTrait}};"
        );
        let _ = writeln!(
            out,
            "            use sea_orm::sea_query::{{Query, Alias, Expr}};"
        );
        let _ = writeln!(
            out,
            "            let mut result: std::collections::HashMap<String, (Vec<String>, u64)> = std::collections::HashMap::new();"
        );
        for (col, _label, limit) in &r.list_filter {
            let _ = writeln!(
                out,
                "            let page_size_{col} = {limit}u64;",
                col = col,
                limit = limit
            );
            let _ = writeln!(
                out,
                "            let cur_page_{col} = pages.get(\"{col}\").copied().unwrap_or(0);",
                col = col
            );
            let _ = writeln!(
                out,
                "            let count_stmt_{col} = Query::select().expr(Expr::cust(\"COUNT(DISTINCT {col})\")).from(Alias::new({module}::Entity.table_name())).and_where(Expr::col(Alias::new(\"{col}\")).is_not_null()).to_owned();",
                col = col,
                module = module
            );
            let _ = writeln!(
                out,
                "            let count_row_{col} = match db.query_one(&count_stmt_{col}).await {{ Ok(r) => r, Err(e) => {{ tracing::error!(\"[runique admin] list_filter `{key}.{col}`: column not found in DB — {{}}\", e); None }} }};",
                col = col,
                key = key
            );
            let _ = writeln!(
                out,
                "            let total_{col} = count_row_{col}.and_then(|r| r.try_get_by_index::<i64>(0).ok()).unwrap_or(0) as u64;",
                col = col
            );
            let _ = writeln!(
                out,
                "            let stmt_{col} = Query::select().distinct().expr(Expr::cust(\"CAST({col} AS TEXT)\")).from(Alias::new({module}::Entity.table_name())).and_where(Expr::col(Alias::new(\"{col}\")).is_not_null()).limit(page_size_{col}).offset(cur_page_{col} * page_size_{col}).to_owned();",
                col = col,
                module = module
            );
            let _ = writeln!(
                out,
                "            let rows_{col} = match db.query_all(&stmt_{col}).await {{ Ok(r) => r, Err(e) => {{ tracing::error!(\"[runique admin] list_filter `{key}.{col}`: column not found in DB — {{}}\", e); vec![] }} }};",
                col = col,
                key = key
            );
            let _ = writeln!(
                out,
                "            let mut vals_{col}: Vec<String> = rows_{col}.iter().filter_map(|r| r.try_get_by_index::<String>(0).ok()).collect(); vals_{col}.sort_by(|a, b| match (a.parse::<i64>(), b.parse::<i64>()) {{ (Ok(x), Ok(y)) => x.cmp(&y), _ => a.cmp(b) }});",
                col = col
            );
            let _ = writeln!(
                out,
                "            result.insert(\"{col}\".to_string(), (vals_{col}, total_{col}));",
                col = col
            );
        }
        let _ = writeln!(out, "            Ok(result)");
        let _ = writeln!(out, "        }})");
        let _ = writeln!(out, "    }});");
        let _ = writeln!(out);
    }

    let _ = writeln!(out, "    registry.register(");
    if r.edit_form_type.is_some() || r.bulk_create.is_some() {
        let _ = writeln!(out, "        ResourceEntry::new(meta, form_builder)");
        let _ = writeln!(
            out,
            "            .with_edit_form_builder(edit_form_builder)"
        );
    } else {
        let _ = writeln!(out, "        ResourceEntry::new(meta, form_builder)");
    }
    let _ = writeln!(out, "            .with_list_fn(list_fn)");
    let _ = writeln!(out, "            .with_get_fn(get_fn)");
    let _ = writeln!(out, "            .with_delete_fn(delete_fn)");
    let _ = writeln!(out, "            .with_create_fn(create_fn)");
    let _ = writeln!(out, "            .with_update_fn(update_fn)");
    let _ = writeln!(
        out,
        "            .with_partial_update_fn(partial_update_fn)"
    );
    let _ = writeln!(out, "            .with_count_fn(count_fn)");
    let _ = writeln!(
        out,
        "            .with_unique_fields({}::UNIQUE_FIELDS)",
        module
    );
    if !r.list_filter.is_empty() {
        let _ = writeln!(out, "            .with_filter_fn(filter_fn)");
    }
    if !r.group_action.is_empty() {
        let actions_str = build_group_actions_str(&r.group_action);
        let _ = writeln!(
            out,
            "            .with_group_actions(vec![{}])",
            actions_str
        );
    }
    if !r.m2m.is_empty() {
        let _ = writeln!(out, "            .with_m2m_loader(m2m_loader)");
    }
    if let Some(ref own_field) = r.own_field {
        let _ = writeln!(out, "            .with_own_field(\"{}\")", own_field);
    }
    let _ = writeln!(out, "    );");
    let _ = writeln!(out);

    Ok(())
}

// ─── Code generation helpers ─────────────────────────────────────────────────

/// Emits a complete `DynForm` impl block for a wrapper struct.
/// `save_param` is the parameter name ("db" or "_db"), `save_body` is the return expression.
fn write_dyn_form_wrapper(
    out: &mut String,
    comment: &str,
    wrapper: &str,
    form_path: &str,
    save_param: &str,
    save_body: &str,
) -> Result<(), String> {
    let _ = writeln!(out, "// ── {} ──", comment);
    let _ = writeln!(out, "struct {}(pub {});", wrapper, form_path);
    let _ = writeln!(out);
    let _ = writeln!(out, "#[async_trait]");
    let _ = writeln!(out, "impl DynForm for {} {{", wrapper);
    let _ = writeln!(out, "    async fn is_valid(&mut self) -> bool {{");
    let _ = writeln!(out, "        self.0.is_valid().await");
    let _ = writeln!(out, "    }}");
    let _ = writeln!(out);
    let _ = writeln!(
        out,
        "    async fn save(&mut self, {}: &DatabaseConnection) -> Result<(), DbErr> {{",
        save_param
    );
    let _ = writeln!(out, "        {}", save_body);
    let _ = writeln!(out, "    }}");
    let _ = writeln!(out);
    let _ = writeln!(out, "    fn get_form(&self) -> &Forms {{");
    let _ = writeln!(out, "        self.0.get_form()");
    let _ = writeln!(out, "    }}");
    let _ = writeln!(out);
    let _ = writeln!(out, "    fn get_form_mut(&mut self) -> &mut Forms {{");
    let _ = writeln!(out, "        self.0.get_form_mut()");
    let _ = writeln!(out, "    }}");
    let _ = writeln!(out, "}}");
    let _ = writeln!(out);
    Ok(())
}

/// Emits a `FormBuilder` closure assigned to `var_name`.
fn write_form_builder_closure(out: &mut String, var_name: &str, form_path: &str, wrapper: &str) {
    let _ = writeln!(
        out,
        "    let {}: FormBuilder = Arc::new(|_db: ADb, _vec: Vec<std::string::String>, data: StrMap, tera: ATera, csrf: String, method: Method| {{",
        var_name
    );
    let _ = writeln!(out, "        Box::pin(async move {{");
    let _ = writeln!(
        out,
        "            let form = {}::build_with_data(&data, tera, &csrf, method).await;",
        form_path
    );
    let _ = writeln!(
        out,
        "            Box::new({}(form)) as Box<dyn DynForm>",
        wrapper
    );
    let _ = writeln!(out, "        }})");
    let _ = writeln!(out, "    }});");
    let _ = writeln!(out);
}

/// Emits a `FormBuilder` closure with optional FK ChoiceField injection from the DB.
/// When `fk_cols` is empty, behaves identically to `write_form_builder_closure`.
fn write_form_builder_closure_fk(
    out: &mut String,
    var_name: &str,
    form_path: &str,
    wrapper: &str,
    fk_cols: &[&(
        String,
        String,
        Option<crate::admin::daemon::parser::FkDisplay>,
    )],
) {
    let db_param = if fk_cols.is_empty() { "_db" } else { "db" };
    let _ = writeln!(
        out,
        "    let {var}: FormBuilder = Arc::new(|{db}: ADb, _vec: Vec<std::string::String>, data: StrMap, tera: ATera, csrf: String, method: Method| {{",
        var = var_name,
        db = db_param,
    );
    let _ = writeln!(out, "        Box::pin(async move {{");
    let mut_kw = if fk_cols.is_empty() { "let" } else { "let mut" };
    let _ = writeln!(
        out,
        "            {mut_kw} form = {form}::build_with_data(&data, tera, &csrf, method).await;",
        mut_kw = mut_kw,
        form = form_path,
    );
    for (col, label, fk_opt) in fk_cols {
        let fk = fk_opt.as_ref().unwrap();
        let safe_var = col.replace('.', "_");
        let _ = writeln!(out, "            {{");
        let _ = writeln!(out, "                use sea_orm::ConnectionTrait;");
        let _ = writeln!(
            out,
            "                let _fk_opt_stmt_{safe} = sea_orm::sea_query::Query::select().expr(sea_orm::sea_query::Expr::cust(\"CAST(id AS TEXT)\")).expr(sea_orm::sea_query::Expr::cust(\"{fk_col}\")).from(sea_orm::sea_query::Alias::new(\"{fk_table}\")).to_owned();",
            safe = safe_var,
            fk_col = fk.col,
            fk_table = fk.table,
        );
        let _ = writeln!(
            out,
            "                let _fk_opt_choices_{safe}: Vec<(String, String)> = db.query_all(&_fk_opt_stmt_{safe}).await.unwrap_or_default().iter().filter_map(|row| {{ let id = row.try_get_by_index::<String>(0).ok()?; let lbl = row.try_get_by_index::<String>(1).ok()?; Some((id, lbl)) }}).collect();",
            safe = safe_var,
        );
        let _ = writeln!(
            out,
            "                form.get_form_mut().field_choices(\"{col}\", \"{label}\", _fk_opt_choices_{safe});",
            col = col,
            label = label,
            safe = safe_var,
        );
        let _ = writeln!(out, "            }}");
    }
    let _ = writeln!(
        out,
        "            Box::new({wrapper}(form)) as Box<dyn DynForm>",
        wrapper = wrapper,
    );
    let _ = writeln!(out, "        }})");
    let _ = writeln!(out, "    }});");
    let _ = writeln!(out);
}

/// Emits a `search_cond!` call for the search block.
/// FK columns are skipped — searching a raw FK ID makes no sense.
fn write_search_conditions(
    out: &mut String,
    list_display: &[(
        String,
        String,
        Option<crate::admin::daemon::parser::FkDisplay>,
    )],
    module: &str,
) {
    let searchable: Vec<&str> = list_display
        .iter()
        .filter(|(_, _, fk)| fk.is_none())
        .map(|(col, _, _)| col.as_str())
        .collect();

    if searchable.is_empty() {
        let _ = writeln!(
            out,
            "                let search_cond = search_cond!({module}::Entity => all_columns icontains search_str);",
            module = module
        );
    } else {
        let cols = searchable
            .iter()
            .map(|col| format!("\"{}\" icontains search_str", col))
            .collect::<Vec<_>>()
            .join(", ");
        let _ = writeln!(
            out,
            "                let search_cond = search_cond!({module}::Entity => or({cols}));",
            module = module,
            cols = cols
        );
    }
}

/// Builds a `DisplayConfig::new().columns_include(...).columns_exclude(...).list_filter(...)` chain.
fn build_display_chain(
    list_display: &[(
        String,
        String,
        Option<crate::admin::daemon::parser::FkDisplay>,
    )],
    list_exclude: &[String],
    list_filter: &[(String, String, u64)],
) -> String {
    let mut chain = "DisplayConfig::new()".to_string();
    if !list_display.is_empty() {
        let cols = list_display
            .iter()
            .map(|(c, l, _)| format!("(\"{}\", \"{}\")", c, l))
            .collect::<Vec<_>>()
            .join(", ");
        chain.push_str(&format!(".columns_include(vec![{}])", cols));
    }
    if !list_exclude.is_empty() {
        let cols = list_exclude
            .iter()
            .map(|c| format!("\"{}\"", c))
            .collect::<Vec<_>>()
            .join(", ");
        chain.push_str(&format!(".columns_exclude(vec![{}])", cols));
    }
    if !list_filter.is_empty() {
        let filters = list_filter
            .iter()
            .map(|(col, label, limit)| format!("(\"{}\", \"{}\", {}u64)", col, label, limit))
            .collect::<Vec<_>>()
            .join(", ");
        chain.push_str(&format!(".list_filter(vec![{}])", filters));
    }
    chain
}

/// Builds a comma-separated list of `GroupAction::*` expressions.
fn build_group_actions_str(group_action: &[(String, String, Option<String>)]) -> String {
    group_action
        .iter()
        .map(|(f, l, v)| match v {
            Some(val) => format!("GroupAction::val(\"{}\", \"{}\", \"{}\")", f, l, val),
            None => format!("GroupAction::bool(\"{}\", \"{}\")", f, l),
        })
        .collect::<Vec<_>>()
        .join(", ")
}

// ─── Path utilities ──────────────────────────────────────────────────────────

fn full_model_path(model_type: &str) -> String {
    if model_type.starts_with("crate::") || model_type.starts_with("runique::") {
        model_type.to_string()
    } else {
        format!("crate::entities::{}", model_type)
    }
}

/// Converts snake_case → PascalCase (e.g., `blog_post` → `BlogPost`)
fn pascal_case(s: &str) -> String {
    s.split('_')
        .map(|word| {
            let mut c = word.chars();
            match c.next() {
                None => String::new(),
                Some(first) => first.to_uppercase().collect::<String>() + c.as_str(),
            }
        })
        .collect()
}

/// Extracts the module name from a model path.
/// Ex: `users::Model` → `users`, `crate::entities::blog_post::Model` → `blog_post`
/// The last segment is the struct name (Model), the second to last is the module.
fn model_to_module(model_type: &str) -> String {
    let segments: Vec<&str> = model_type.split("::").collect();
    if segments.len() >= 2 {
        segments[segments.len().saturating_sub(2)].to_string()
    } else {
        // Fallback: PascalCase → snake_case for the single segment
        let base = segments.last().copied().unwrap_or(model_type);
        let mut result = String::new();
        for (i, c) in base.chars().enumerate() {
            if c.is_uppercase() && i > 0 {
                result.push('_');
            }
            result.extend(c.to_lowercase());
        }
        result
    }
}
