//! `smith make:*` scaffolding subcommands.

use std::fs;
use std::path::PathBuf;

use anyhow::{Context, Result};
use handlebars::Handlebars;
use serde_json::json;

use super::project_root;

pub fn model(name: &str, with_migration: bool, fields: &[String]) -> Result<()> {
    let path = project_root().join(format!("app/Models/{name}.rs"));
    let fields_parsed = parse_fields(fields);
    write_template(
        &path,
        MODEL_TEMPLATE,
        json!({
            "name": name,
            "table": pluralize_snake(&snake_case(name)),
            "fields": fields_parsed,
        }),
    )?;
    println!("created {}", path.display());

    if with_migration {
        migration(&format!(
            "create_{}_table",
            pluralize_snake(&snake_case(name))
        ))?;
    }
    Ok(())
}

pub fn migration(name: &str) -> Result<()> {
    let ts = chrono::Utc::now().format("%Y_%m_%d_%H%M%S");
    let file_name = format!("{ts}_{name}.rs");
    let stem = format!("{ts}_{name}");
    let path = project_root().join("database/migrations").join(&file_name);

    // Try to infer "create_X_table" → X
    let table = name
        .strip_prefix("create_")
        .and_then(|s| s.strip_suffix("_table"))
        .map(|s| s.to_string());

    let struct_name = pascal_case(name);
    write_template(
        &path,
        MIGRATION_TEMPLATE,
        json!({
            "struct_name": struct_name,
            "name": stem,
            "table": table,
        }),
    )?;
    println!("created {}", path.display());

    // Auto-append the `#[path = "..."] pub mod ...;` line to database/migrations/mod.rs.
    // Inventory auto-discovers from there. No manual `all()` Vec needed.
    let mod_rs = project_root().join("database/migrations/mod.rs");
    let snake = snake_case(name);
    let mut mod_name = snake.clone();
    let mut existing = if mod_rs.exists() {
        std::fs::read_to_string(&mod_rs).unwrap_or_default()
    } else {
        String::new()
    };
    // If the snake_case name already appears as a `pub mod <name>;`, fall back to
    // the timestamp-suffixed form to avoid collisions. Running `make:migration` with
    // the same name twice is the only realistic way to trigger this.
    let collision_pat = format!("pub mod {snake};");
    if existing.contains(&collision_pat) {
        mod_name = format!("{snake}_{}", ts.to_string().replace('_', ""));
    }
    let mod_line = format!("\n#[path = \"{file_name}\"]\npub mod {mod_name};\n");
    if mod_rs.exists() {
        if !existing.contains(&file_name) {
            if !existing.ends_with('\n') {
                existing.push('\n');
            }
            existing.push_str(&mod_line);
            std::fs::write(&mod_rs, existing).context("append migration to mod.rs")?;
            println!("appended to database/migrations/mod.rs");
        }
    } else {
        // First migration in a fresh project — create the mod.rs.
        std::fs::create_dir_all(mod_rs.parent().unwrap()).ok();
        std::fs::write(
            &mod_rs,
            format!(
                "//! Database migrations. Each file is `mod`-included here. \n//! `MigrationRunner::new(pool)` auto-discovers via inventory.\n{mod_line}"
            ),
        )
        .context("write database/migrations/mod.rs")?;
    }
    Ok(())
}

pub fn controller(name: &str, resource: bool) -> Result<()> {
    let dir = "app/Http/Controllers";
    let path = project_root().join(format!("{dir}/{name}.rs"));
    let tpl = if resource {
        RESOURCE_CONTROLLER_TEMPLATE
    } else {
        CONTROLLER_TEMPLATE
    };
    let resource_lower = snake_case(name.trim_end_matches("Controller"));
    write_template(
        &path,
        tpl,
        json!({
            "name": name,
            "resource": resource_lower,
            "resource_plural": pluralize_snake(&resource_lower),
        }),
    )?;
    println!("created {}", path.display());
    append_to_mod_rs(dir, name)?;
    Ok(())
}

pub fn request(name: &str) -> Result<()> {
    let dir = "app/Http/Requests";
    let path = project_root().join(format!("{dir}/{name}.rs"));
    write_template(&path, REQUEST_TEMPLATE, json!({ "name": name }))?;
    println!("created {}", path.display());
    append_to_mod_rs(dir, name)?;
    Ok(())
}

pub fn job(name: &str) -> Result<()> {
    let dir = "app/Jobs";
    let path = project_root().join(format!("{dir}/{name}.rs"));
    write_template(&path, JOB_TEMPLATE, json!({ "name": name }))?;
    println!("created {}", path.display());
    append_to_mod_rs(dir, name)?;
    Ok(())
}

pub fn event(name: &str) -> Result<()> {
    let dir = "app/Events";
    let path = project_root().join(format!("{dir}/{name}.rs"));
    write_template(&path, EVENT_TEMPLATE, json!({ "name": name }))?;
    println!("created {}", path.display());
    append_to_mod_rs(dir, name)?;
    Ok(())
}

pub fn listener(name: &str, event: Option<&str>) -> Result<()> {
    let dir = "app/Listeners";
    let path = project_root().join(format!("{dir}/{name}.rs"));
    write_template(
        &path,
        LISTENER_TEMPLATE,
        json!({
            "name": name,
            "event": event.unwrap_or("SomeEvent"),
        }),
    )?;
    println!("created {}", path.display());
    append_to_mod_rs(dir, name)?;
    Ok(())
}

pub fn test(name: &str) -> Result<()> {
    let path = project_root().join(format!("tests/{}.rs", snake_case(name)));
    write_template(&path, TEST_TEMPLATE, json!({ "name": name }))?;
    println!("created {}", path.display());
    Ok(())
}

pub fn seeder(name: &str) -> Result<()> {
    let path = project_root().join(format!("database/seeders/{name}.rs"));
    write_template(&path, SEEDER_TEMPLATE, json!({ "name": name }))?;
    println!("created {}", path.display());

    // Auto-append the `#[path] pub mod foo;` line to database/seeders/mod.rs.
    // Inventory auto-discovers via `#[derive(Seeder)]` — no manual registration.
    let mod_rs = project_root().join("database/seeders/mod.rs");
    let mod_name = snake_case(name);
    let line =
        format!("\n#[path = \"{name}.rs\"]\npub mod {mod_name};\npub use {mod_name}::{name};\n");
    if mod_rs.exists() {
        let mut current = std::fs::read_to_string(&mod_rs).unwrap_or_default();
        if !current.contains(&format!("\"{name}.rs\"")) {
            if !current.ends_with('\n') {
                current.push('\n');
            }
            current.push_str(&line);
            std::fs::write(&mod_rs, current).context("append seeder to mod.rs")?;
            println!("appended to database/seeders/mod.rs");
        }
    }
    println!();
    println!("  smith db:seed --class={name}");
    println!();
    Ok(())
}

pub fn component(name: &str) -> Result<()> {
    let snake = snake_case(name);
    let rust_path = project_root().join(format!("app/Spark/{name}.rs"));
    let view_path = project_root().join(format!("resources/views/spark/{snake}.forge.html"));
    write_template(
        &rust_path,
        COMPONENT_RUST_TEMPLATE,
        json!({ "name": name, "snake": snake.clone() }),
    )?;
    println!("created {}", rust_path.display());
    write_template(
        &view_path,
        COMPONENT_VIEW_TEMPLATE,
        json!({ "name": name, "snake": snake.clone() }),
    )?;
    println!("created {}", view_path.display());

    // Auto-include `pub mod <snake>;` in app/Spark/mod.rs (create if missing).
    let mod_rs = project_root().join("app/Spark/mod.rs");
    let mod_name = snake.clone();
    let line =
        format!("\n#[path = \"{name}.rs\"]\npub mod {mod_name};\npub use {mod_name}::{name};\n");
    if mod_rs.exists() {
        let mut current = std::fs::read_to_string(&mod_rs).unwrap_or_default();
        if !current.contains(&format!("\"{name}.rs\"")) {
            if !current.ends_with('\n') {
                current.push('\n');
            }
            current.push_str(&line);
            std::fs::write(&mod_rs, current).context("append component to mod.rs")?;
            println!("appended to app/Spark/mod.rs");
        }
    } else {
        std::fs::create_dir_all(mod_rs.parent().unwrap()).ok();
        std::fs::write(
            &mod_rs,
            format!(
                "//! Spark components. Each module is `mod`-included here.\n//! Components register themselves via `inventory` from `#[spark_component]`.\n{line}"
            ),
        )
        .context("write app/Spark/mod.rs")?;
    }
    println!();
    println!("  Mount it in a template:");
    println!("    @spark(\"{snake}\")");
    println!();
    Ok(())
}

pub fn mail(name: &str) -> Result<()> {
    let dir = "app/Mail";
    let path = project_root().join(format!("{dir}/{name}.rs"));
    write_template(&path, MAIL_TEMPLATE, json!({ "name": name }))?;
    println!("created {}", path.display());
    append_to_mod_rs(dir, name)?;
    Ok(())
}

pub fn notification(name: &str) -> Result<()> {
    let dir = "app/Notifications";
    let path = project_root().join(format!("{dir}/{name}.rs"));
    write_template(&path, NOTIFICATION_TEMPLATE, json!({ "name": name }))?;
    println!("created {}", path.display());
    append_to_mod_rs(dir, name)?;
    Ok(())
}

pub fn policy(name: &str, model: Option<&str>) -> Result<()> {
    // Infer model from policy name: PostPolicy → Post (default).
    let model_name = model.unwrap_or_else(|| name.strip_suffix("Policy").unwrap_or(name));
    let dir = "app/Policies";
    let path = project_root().join(format!("{dir}/{name}.rs"));
    write_template(
        &path,
        POLICY_TEMPLATE,
        json!({ "name": name, "model": model_name }),
    )?;
    println!("created {}", path.display());
    append_to_mod_rs(dir, name)?;
    Ok(())
}

pub fn rule(name: &str) -> Result<()> {
    let dir = "app/Rules";
    let path = project_root().join(format!("{dir}/{name}.rs"));
    write_template(&path, RULE_TEMPLATE, json!({ "name": name }))?;
    println!("created {}", path.display());
    append_to_mod_rs(dir, name)?;
    Ok(())
}

pub fn resource_serializer(name: &str) -> Result<()> {
    // Infer model from resource name: PostResource → Post (default).
    let model_name = name.strip_suffix("Resource").unwrap_or(name);
    let dir = "app/Http/Resources";
    let path = project_root().join(format!("{dir}/{name}.rs"));
    write_template(
        &path,
        RESOURCE_TEMPLATE,
        json!({ "name": name, "model": model_name }),
    )?;
    println!("created {}", path.display());
    append_to_mod_rs(dir, name)?;
    Ok(())
}

pub fn factory(name: &str, model: Option<&str>) -> Result<()> {
    // Infer model from factory name: PostFactory → Post (default).
    let model_name = model.unwrap_or_else(|| name.strip_suffix("Factory").unwrap_or(name));
    let dir = "database/factories";
    let path = project_root().join(format!("{dir}/{name}.rs"));
    write_template(
        &path,
        FACTORY_TEMPLATE,
        json!({ "name": name, "model": model_name }),
    )?;
    println!("created {}", path.display());
    append_to_mod_rs(dir, name)?;
    Ok(())
}

/// Append `#[path = "<Name>.rs"] mod <snake>; pub use <snake>::<Name>;` to the
/// `mod.rs` of `dir`. Idempotent: skips when the file is already referenced.
/// Creates `mod.rs` if it doesn't exist (with a one-line header).
fn append_to_mod_rs(dir: &str, name: &str) -> Result<()> {
    let mod_rs = project_root().join(dir).join("mod.rs");
    let snake = snake_case(name);
    let line = format!("\n#[path = \"{name}.rs\"]\nmod {snake};\npub use {snake}::{name};\n");
    let file_marker = format!("\"{name}.rs\"");

    let mut current = if mod_rs.exists() {
        std::fs::read_to_string(&mod_rs).unwrap_or_default()
    } else {
        if let Some(parent) = mod_rs.parent() {
            std::fs::create_dir_all(parent).ok();
        }
        String::new()
    };

    if current.contains(&file_marker) {
        return Ok(());
    }

    if !current.is_empty() && !current.ends_with('\n') {
        current.push('\n');
    }
    current.push_str(&line);
    std::fs::write(&mod_rs, current).with_context(|| format!("append {name} to {}/mod.rs", dir))?;
    println!("appended to {}/mod.rs", dir);
    Ok(())
}

fn parse_fields(fields: &[String]) -> serde_json::Value {
    let mut parsed = Vec::new();
    for spec in fields {
        let parts: Vec<&str> = spec.split(':').collect();
        let name = parts.first().copied().unwrap_or("").to_string();
        let ty = parts.get(1).copied().unwrap_or("string").to_string();
        let modifier = parts.get(2).copied().unwrap_or("").to_string();
        parsed.push(json!({
            "name": name,
            "type": ty,
            "rust_type": rust_type_for(&ty),
            "modifier": modifier,
        }));
    }
    serde_json::Value::Array(parsed)
}

fn rust_type_for(ty: &str) -> &'static str {
    match ty {
        "string" | "text" => "String",
        "int" | "integer" => "i32",
        "bigint" | "big_integer" => "i64",
        "bool" | "boolean" => "bool",
        "uuid" => "uuid::Uuid",
        "json" => "serde_json::Value",
        "timestamp" | "datetime" => "chrono::DateTime<chrono::Utc>",
        _ => "String",
    }
}

fn write_template(path: &PathBuf, template: &str, data: serde_json::Value) -> Result<()> {
    let mut hb = Handlebars::new();
    hb.register_escape_fn(handlebars::no_escape);
    let rendered = hb
        .render_template(template, &data)
        .context("template render failed")?;
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent).ok();
    }
    if path.exists() {
        anyhow::bail!("file already exists: {}", path.display());
    }
    fs::write(path, rendered).context("write file failed")?;
    Ok(())
}

fn snake_case(s: &str) -> String {
    let mut out = String::new();
    for (i, c) in s.chars().enumerate() {
        if c.is_uppercase() && i > 0 {
            out.push('_');
        }
        out.push(c.to_ascii_lowercase());
    }
    out
}

fn pascal_case(s: &str) -> String {
    s.split('_')
        .map(|w| {
            let mut chars = w.chars();
            match chars.next() {
                Some(c) => c.to_uppercase().chain(chars).collect::<String>(),
                None => String::new(),
            }
        })
        .collect()
}

fn pluralize_snake(s: &str) -> String {
    if s.ends_with('s') {
        s.to_string()
    } else if s.ends_with('y') {
        let mut s = s.to_string();
        s.pop();
        s.push_str("ies");
        s
    } else {
        format!("{s}s")
    }
}

const MODEL_TEMPLATE: &str = r#"use anvilforge::cast::Model;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize, Model)]
#[table("{{table}}")]
pub struct {{name}} {
    pub id: i64,
{{#each fields}}    pub {{this.name}}: {{this.rust_type}},
{{/each}}
    pub created_at: Option<chrono::DateTime<chrono::Utc>>,
    pub updated_at: Option<chrono::DateTime<chrono::Utc>>,
}
"#;

const MIGRATION_TEMPLATE: &str = r#"use anvilforge::prelude::*;
use anvilforge::cast::Schema;

#[derive(Migration)]
pub struct {{struct_name}};

impl CastMigration for {{struct_name}} {
    fn name(&self) -> &'static str {
        "{{name}}"
    }

    fn up(&self, s: &mut Schema) {
{{#if table}}        s.create("{{table}}", |t| {
            t.id();
            t.timestamps();
        });
{{else}}        // TODO: define migration up
{{/if}}    }

    fn down(&self, s: &mut Schema) {
{{#if table}}        s.drop_if_exists("{{table}}");
{{else}}        // TODO: define migration down
{{/if}}    }
}
"#;

const CONTROLLER_TEMPLATE: &str = r#"use anvilforge::prelude::*;

pub struct {{name}};

impl {{name}} {
    pub async fn index(State(_container): State<Container>) -> Result<ViewResponse> {
        // TODO: implement
        Ok(ViewResponse::new("<h1>{{name}}</h1>"))
    }
}
"#;

const RESOURCE_CONTROLLER_TEMPLATE: &str = r#"use anvilforge::prelude::*;

pub struct {{name}};

impl {{name}} {
    pub async fn index(State(_c): State<Container>) -> Result<ViewResponse> {
        Ok(ViewResponse::new("<h1>{{resource_plural}}#index</h1>"))
    }

    pub async fn show(Path(id): Path<i64>) -> Result<ViewResponse> {
        Ok(ViewResponse::new(format!("<h1>{{resource}}#show {{{{id}}}}</h1>")))
    }

    pub async fn create() -> Result<ViewResponse> {
        Ok(ViewResponse::new("<h1>{{resource}}#create</h1>"))
    }

    pub async fn store() -> Result<Redirect> {
        Ok(Redirect::to("/{{resource_plural}}"))
    }

    pub async fn edit(Path(_id): Path<i64>) -> Result<ViewResponse> {
        Ok(ViewResponse::new("<h1>{{resource}}#edit</h1>"))
    }

    pub async fn update(Path(_id): Path<i64>) -> Result<Redirect> {
        Ok(Redirect::to("/{{resource_plural}}"))
    }

    pub async fn destroy(Path(_id): Path<i64>) -> Result<Redirect> {
        Ok(Redirect::to("/{{resource_plural}}"))
    }
}
"#;

const REQUEST_TEMPLATE: &str = r#"use anvilforge::prelude::*;
use garde::Validate;
use serde::Deserialize;

#[derive(Debug, Deserialize, Validate, FormRequest)]
pub struct {{name}} {
    #[garde(length(min = 1))]
    pub title: String,
}
"#;

const JOB_TEMPLATE: &str = r#"use anvilforge::prelude::*;
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize, Job)]
pub struct {{name}} {
    // TODO: job payload fields
}

impl {{name}} {
    pub async fn handle(&self, _container: &Container) -> Result<()> {
        // TODO: implement
        Ok(())
    }
}
"#;

const EVENT_TEMPLATE: &str = r#"use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct {{name}} {
    // TODO: event payload fields
}
"#;

const LISTENER_TEMPLATE: &str = r#"use anvilforge::prelude::*;
use crate::app::events::{{event}};

pub struct {{name}};

impl {{name}} {
    pub async fn handle(_event: {{event}}) -> Result<()> {
        // TODO: implement
        Ok(())
    }
}
"#;

const TEST_TEMPLATE: &str = r#"use anvilforge::assay::*;

#[tokio::test]
async fn {{name}}_works() {
    let app = crate::bootstrap::app::build(/* container */).await
        .expect("build app");
    let client = TestClient::new(app).await;

    client.get("/").await
        .assert_ok()
        .assert_see("welcome");

    // Fluent expectations á la Pest:
    expect(2 + 2).to_be(4);
    expect(vec!["a", "b", "c"]).to_have_length(3);
}
"#;

const SEEDER_TEMPLATE: &str = r#"//! {{name}}. Auto-registered via `#[derive(Seeder)]`.

use anvilforge::prelude::*;
use anvilforge::seeder::Seeder;
use anvilforge::async_trait::async_trait;

#[derive(Seeder)]
pub struct {{name}};

#[async_trait]
impl Seeder for {{name}} {
    fn name(&self) -> &'static str { "{{name}}" }

    async fn run(&self, _c: &Container) -> Result<()> {
        // TODO: write seed data, e.g.:
        //   sqlx::query("INSERT INTO ... ON CONFLICT DO NOTHING ...")
        //       .execute(_c.pool()).await.map_err(Error::Database)?;
        Ok(())
    }
}
"#;

const COMPONENT_RUST_TEMPLATE: &str = r#"//! {{name}} — Spark reactive component.

use anvilforge::prelude::*;
use spark::prelude::*;

#[spark_component(template = "spark/{{snake}}")]
pub struct {{name}} {
    pub count: i32,
}

#[spark_actions]
impl {{name}} {
    #[spark_mount]
    fn mount(_props: MountProps) -> Self {
        Self::default()
    }

    async fn increment(&mut self) -> Result<()> {
        self.count += 1;
        Ok(())
    }
}
"#;

const COMPONENT_VIEW_TEMPLATE: &str = r#"<div>
    <h2>{{ '{{ count }}' }}</h2>
    <button spark:click="increment">+1</button>
</div>
"#;

const FACTORY_TEMPLATE: &str = r#"//! {{name}} — generates random {{model}}s for tests/dev.

use anvilforge::prelude::*;
use anvilforge::seeder::{Factory, PersistentFactory};
use anvilforge::async_trait::async_trait;

use crate::app::Models::{{model}};

pub struct {{name}};

impl Factory<{{model}}> for {{name}} {
    fn definition() -> {{model}} {
        use fake::{Fake, faker::{name::en::Name, internet::en::SafeEmail}};
        // TODO: adjust field assignments to match {{model}}'s fields.
        {{model}} {
            id: 0,
            name: Name().fake(),
            email: SafeEmail().fake(),
            ..Default::default()
        }
    }
}

// Implement PersistentFactory to enable `{{name}}::create(&c).await?`.
#[async_trait]
impl PersistentFactory<{{model}}> for {{name}} {
    async fn save(_c: &Container, _model: {{model}}) -> Result<{{model}}> {
        // TODO: insert + return the row with the assigned id.
        // Example for a User-shaped model:
        //   let row: (i64,) = sqlx::query_as(
        //       "INSERT INTO {{model | lower}}s (name, email) VALUES ($1, $2) RETURNING id",
        //   )
        //   .bind(&_model.name).bind(&_model.email)
        //   .fetch_one(_c.pool()).await.map_err(Error::Database)?;
        //   Ok({{model}} { id: row.0, .._model })
        Ok(_model)
    }
}
"#;

// ─── Laravel-parity scaffolders (mail / notification / policy / rule / resource) ───

const MAIL_TEMPLATE: &str = r#"//! {{name}} — a Mailable. Laravel parity: `Mail::to($user)->send(new {{name}}($args))`.

use anvilforge::prelude::*;
use anvilforge::mail::{Mailable, OutgoingMessage};

pub struct {{name}} {
    // TODO: typed fields the template / subject need.
    // pub order: Order,
}

impl Mailable for {{name}} {
    fn build(&self, message: &mut OutgoingMessage) {
        message
            .subject("TODO: subject")
            .view("mail.{{name}}", serde_json::json!({
                // "order": self.order,
            }));
    }
}

// To send:
//   c.mailer().to(&user).send({{name}} { /* ... */ }).await?;
"#;

const NOTIFICATION_TEMPLATE: &str = r#"//! {{name}} — a multi-channel Notification (mail + database + broadcast).

use anvilforge::prelude::*;
use anvilforge::notification::{Notification, NotificationChannel, NotifiablePayload};

pub struct {{name}} {
    // TODO: typed fields. The same struct serves every channel.
}

impl Notification for {{name}} {
    fn via(&self) -> Vec<NotificationChannel> {
        // Pick the channels this notification is sent over.
        vec![NotificationChannel::Mail]
    }

    fn to_mail(&self) -> NotifiablePayload {
        NotifiablePayload::mail("TODO: subject", "mail.{{name}}", serde_json::json!({}))
    }

    fn to_database(&self) -> NotifiablePayload {
        NotifiablePayload::database(serde_json::json!({}))
    }
}

// To send:
//   c.notify(&users, {{name}} { /* ... */ }).await?;
"#;

const POLICY_TEMPLATE: &str = r#"//! {{name}} — authorization policy for `{{model}}`.
//! Laravel parity: `class {{name}} { public function view(User $u, {{model}} $m) { ... } }`.

use anvilforge::prelude::*;
use crate::app::Models::{User, {{model}}};

pub struct {{name}};

impl {{name}} {
    /// Anyone can list — return true. Restrict by querying inside the controller.
    pub fn view_any(_user: &User) -> bool {
        true
    }

    pub fn view(_user: &User, _resource: &{{model}}) -> bool {
        // TODO: per-resource visibility rule.
        true
    }

    pub fn create(_user: &User) -> bool {
        // TODO: role check.
        true
    }

    pub fn update(_user: &User, _resource: &{{model}}) -> bool {
        // TODO: ownership check. e.g. _user.id == _resource.author_id
        false
    }

    pub fn delete(_user: &User, _resource: &{{model}}) -> bool {
        false
    }
}

// To check inside a controller:
//   if !{{name}}::update(&user, &resource) {
//       return Err(Error::forbidden("not yours"));
//   }
"#;

const RULE_TEMPLATE: &str = r#"//! {{name}} — custom validation rule.
//! Laravel parity: `class {{name}} implements Rule { public function passes($attr, $value) { ... } }`.
//!
//! Use it on a FormRequest field with `#[garde(custom = "{{name}}::check"))]`,
//! or call `{{name}}::check(&value)` directly.

use garde::Path;

pub struct {{name}};

impl {{name}} {
    pub fn check(value: &str, _ctx: &()) -> Result<(), garde::Error> {
        // TODO: return Err(garde::Error::new("message")) on failure.
        if value.is_empty() {
            return Err(garde::Error::new("must not be empty"));
        }
        Ok(())
    }
}

#[allow(dead_code)]
fn _example(_p: Path) {}
"#;

const RESOURCE_TEMPLATE: &str = r#"//! {{name}} — API Resource serializer for `{{model}}`.
//! Laravel parity: `class {{name}} extends JsonResource { public function toArray($req) { ... } }`.
//!
//! Wrap a model instance in this to control the JSON shape sent to clients,
//! independent of the database column layout.

use anvilforge::prelude::*;
use serde::Serialize;
use crate::app::Models::{{model}};

#[derive(Serialize)]
pub struct {{name}} {
    pub id: i64,
    // TODO: the fields you want to expose.
    // Hide secrets (password, internal flags) by simply not listing them.
}

impl From<{{model}}> for {{name}} {
    fn from(m: {{model}}) -> Self {
        Self {
            id: m.id,
            // TODO: map each field from the model.
        }
    }
}

impl {{name}} {
    /// Convenience for collection endpoints: `Json({{name}}::collection(rows))`.
    pub fn collection(rows: Vec<{{model}}>) -> Vec<{{name}}> {
        rows.into_iter().map({{name}}::from).collect()
    }
}
"#;
