//! `rok make:*` — code generator commands.

use std::{fs, path::Path};

use heck::{ToSnakeCase, ToUpperCamelCase};

// ── Controller ────────────────────────────────────────────────────────────────

pub fn controller(name: &str, resource: bool) -> anyhow::Result<()> {
    let camel = name.to_upper_camel_case();
    let base = camel.strip_suffix("Controller").unwrap_or(&camel);
    let pascal = base.to_upper_camel_case();
    let snake = base.to_snake_case();
    let path = format!("src/app/controllers/{snake}_controller.rs");

    if resource {
        write_file(&path, &resource_controller_template(&pascal, &snake))?;
    } else {
        write_file(&path, &controller_template(&pascal, &snake))?;
    }
    println!("Created  {path}");
    println!("Register it in src/app/controllers/mod.rs:");
    println!("  pub mod {snake}_controller;");
    Ok(())
}

fn controller_template(pascal: &str, snake: &str) -> String {
    format!(
        r#"use axum::{{extract::Path}};
use rok_auth::axum::Ctx;
use rok_auth_macros::controller;
use rok_core::api::ApiResponse;

#[controller]
pub struct {pascal}Controller;

impl {pascal}Controller {{
    /// `GET /{snake}s`
    pub async fn index(ctx: Ctx) -> ApiResponse {{
        ApiResponse::ok(serde_json::json!([]))
    }}

    /// `POST /{snake}s`
    pub async fn store(ctx: Ctx) -> ApiResponse {{
        ApiResponse::created(serde_json::json!({{ "message": "created" }}))
    }}

    /// `GET /{snake}s/:id`
    pub async fn show(ctx: Ctx, Path(id): Path<i64>) -> ApiResponse {{
        ApiResponse::ok(serde_json::json!({{ "id": id }}))
    }}

    /// `PUT /{snake}s/:id`
    pub async fn update(ctx: Ctx, Path(id): Path<i64>) -> ApiResponse {{
        ApiResponse::ok(serde_json::json!({{ "id": id, "message": "updated" }}))
    }}

    /// `DELETE /{snake}s/:id`
    pub async fn destroy(ctx: Ctx, Path(id): Path<i64>) -> ApiResponse {{
        ApiResponse::no_content()
    }}
}}
"#
    )
}

fn resource_controller_template(pascal: &str, snake: &str) -> String {
    format!(
        r#"use axum::{{
    extract::{{Path, State}},
    Json,
}};
use rok_auth::axum::Ctx;
use rok_auth_macros::controller;
use rok_core::api::ApiResponse;
use crate::app::models::{pascal};
use crate::app::resources::{snake}_resource::{pascal}Resource;
use crate::app::validators::{{create_{snake}_request::Create{pascal}Request, update_{snake}_request::Update{pascal}Request}};
use crate::state::AppState;

#[controller]
pub struct {pascal}Controller;

impl {pascal}Controller {{
    pub async fn index(ctx: Ctx, State(state): State<AppState>) -> ApiResponse {{
        let items = {pascal}::all(&state.pool).await?;
        let data: Vec<_> = items.into_iter().map({pascal}Resource::from).collect();
        ApiResponse::ok(data)
    }}

    pub async fn store(
        ctx: Ctx,
        State(state): State<AppState>,
        Json(body): Json<Create{pascal}Request>,
    ) -> ApiResponse {{
        let item = {pascal}::create(&state.pool, body).await?;
        ApiResponse::created({pascal}Resource::from(item))
    }}

    pub async fn show(
        ctx: Ctx,
        State(state): State<AppState>,
        Path(id): Path<i64>,
    ) -> ApiResponse {{
        let item = {pascal}::find_or_fail(&state.pool, id).await?;
        ApiResponse::ok({pascal}Resource::from(item))
    }}

    pub async fn update(
        ctx: Ctx,
        State(state): State<AppState>,
        Path(id): Path<i64>,
        Json(body): Json<Update{pascal}Request>,
    ) -> ApiResponse {{
        let item = {pascal}::update(&state.pool, id, body).await?;
        ApiResponse::ok({pascal}Resource::from(item))
    }}

    pub async fn destroy(
        ctx: Ctx,
        State(state): State<AppState>,
        Path(id): Path<i64>,
    ) -> ApiResponse {{
        {pascal}::destroy(&state.pool, id).await?;
        ApiResponse::no_content()
    }}
}}
"#
    )
}

// ── Model ─────────────────────────────────────────────────────────────────────

#[allow(dead_code)]
pub fn model(name: &str, id_type: Option<&str>) -> anyhow::Result<()> {
    model_full(name, id_type, false, false, false, false)
}

/// Extended `make:model` — optionally also generates migration, controller, resource, factory.
pub fn model_full(
    name: &str,
    id_type: Option<&str>,
    with_migration: bool,
    with_controller: bool,
    with_resource: bool,
    with_factory: bool,
) -> anyhow::Result<()> {
    let pascal = name.to_upper_camel_case();
    let snake = name.to_snake_case();
    let path = format!("src/app/models/{snake}.rs");

    write_file(&path, &model_template(&pascal, &snake, id_type))?;
    println!("Created  {path}");
    println!("  Register in src/app/models/mod.rs: pub mod {snake};");

    if with_migration {
        migration(&format!("create_{snake}s_table"))?;
    }
    if with_controller {
        controller(name, false)?;
    }
    if with_resource {
        resource(name, false)?;
    }
    if with_factory {
        make_factory(name)?;
    }
    Ok(())
}

/// Full scaffold: model + migration + resource controller + resource + create/update requests +
/// policy + service + test, then prints a routes registration hint.
pub fn model_all(name: &str, id_type: Option<&str>) -> anyhow::Result<()> {
    let pascal = name.to_upper_camel_case();
    let snake = name.to_snake_case();

    // model
    let model_path = format!("src/app/models/{snake}.rs");
    write_file(&model_path, &model_template(&pascal, &snake, id_type))?;
    println!("Created  {model_path}");

    // migration
    migration(&format!("create_{snake}s_table"))?;

    // resource controller (references requests + resource)
    controller(name, true)?;

    // resource transformer
    resource(name, false)?;

    // create / update request stubs
    make_request_pair(name)?;

    // policy
    policy(name, false)?;

    // service
    make_service(name, false)?;

    // test
    test(name, false)?;

    println!();
    println!("Routes hint — add to your router:");
    println!("  Route::resource(\"/{snake}s\", {pascal}Controller)");
    println!();
    println!("Register modules:");
    println!("  src/app/models/mod.rs          → pub mod {snake};");
    println!("  src/app/controllers/mod.rs     → pub mod {snake}_controller;");
    println!("  src/app/resources/mod.rs       → pub mod {snake}_resource;");
    println!("  src/app/requests/mod.rs        → pub mod create_{snake}_request;");
    println!("  src/app/requests/mod.rs        → pub mod update_{snake}_request;");
    println!("  src/app/policies/mod.rs        → pub mod {snake};");
    println!("  src/app/services/mod.rs        → pub mod {snake}_service;");
    Ok(())
}

// ── Factory ───────────────────────────────────────────────────────────────────

pub fn make_factory(name: &str) -> anyhow::Result<()> {
    let pascal = name.to_upper_camel_case();
    let snake = name.to_snake_case();
    let path = format!("src/database/factories/{snake}_factory.rs");
    write_file(&path, &factory_template(&pascal))?;
    println!("Created  {path}");
    Ok(())
}

fn factory_template(pascal: &str) -> String {
    format!(
        r#"use rok_orm::factory::Factory;

pub struct {pascal}Factory;

impl Factory for {pascal}Factory {{
    type Model = super::{pascal};

    fn definition() -> Vec<(&'static str, rok_orm::SqlValue)> {{
        vec![
            // ("name", "Default Name".into()),
        ]
    }}
}}
"#
    )
}

fn model_template(pascal: &str, snake: &str, id_type: Option<&str>) -> String {
    let (use_line, id_rust_type, id_attr) = match id_type {
        Some("ulid") => (
            "use rok_core::crypto::ids::Ulid;\n",
            "Ulid",
            "#[rok_orm(id = \"ulid\")]\n",
        ),
        Some("cuid2") => (
            "use rok_core::crypto::ids::Cuid2;\n",
            "Cuid2",
            "#[rok_orm(id = \"cuid2\")]\n",
        ),
        Some("uuid_v7") => (
            "use rok_core::crypto::ids::UuidV7;\n",
            "UuidV7",
            "#[rok_orm(id = \"uuid_v7\")]\n",
        ),
        Some("snowflake") => (
            "use rok_core::crypto::ids::Snowflake;\n",
            "Snowflake",
            "#[rok_orm(id = \"snowflake\")]\n",
        ),
        Some("nanoid") => (
            "use rok_core::crypto::ids::NanoId;\n",
            "NanoId",
            "#[rok_orm(id = \"nanoid\")]\n",
        ),
        _ => ("", "i64", ""),
    };

    let migration_hint = match id_type {
        Some("ulid") => "// Migration: t.ulid_pk();".to_string(),
        Some("cuid2") => "// Migration: t.cuid2_pk();".to_string(),
        Some("uuid_v7") => "// Migration: t.uuid_v7_pk();".to_string(),
        Some("snowflake") => "// Migration: t.snowflake_pk();".to_string(),
        Some("nanoid") => "// Migration: t.nanoid_pk();".to_string(),
        _ => "// Migration: t.id();".to_string(),
    };

    let _ = snake; // used for future expansion
    format!(
        r#"use rok_orm::Model;
use serde::{{Deserialize, Serialize}};
{use_line}
#[derive(Debug, Clone, Model, Serialize, Deserialize, sqlx::FromRow)]
{id_attr}pub struct {pascal} {{
    pub id: {id_rust_type},
    {migration_hint}
    // TODO: add fields
}}
"#
    )
}

// ── Migration ─────────────────────────────────────────────────────────────────

fn migration_dir() -> &'static str {
    if Path::new("database/migrations").exists() {
        "database/migrations"
    } else {
        "migrations"
    }
}

pub fn migration(name: &str) -> anyhow::Result<()> {
    use chrono::Local;

    let snake = name.to_snake_case();
    let pascal = snake.to_upper_camel_case();
    let ts = Local::now().format("%Y_%m_%d_%H%M%S").to_string();
    let dir = migration_dir();
    fs::create_dir_all(dir)?;
    let filename = format!("{dir}/{ts}_{snake}.rs");

    write_file(&filename, &migration_template(&pascal, &snake, &ts))?;
    println!("Created  {filename}");
    println!("Register in your migration runner:");
    println!("  .migration({pascal})");
    Ok(())
}

fn migration_template(pascal: &str, snake: &str, timestamp: &str) -> String {
    format!(
        r#"use rok_orm::{{Migration, SchemaExecutor}};
use async_trait::async_trait;

pub struct {pascal};

#[async_trait]
impl Migration for {pascal} {{
    fn name(&self) -> &str {{
        "{timestamp}_{snake}"
    }}

    async fn up(&self, schema: &SchemaExecutor) -> anyhow::Result<()> {{
        schema.create("{snake}", |t| {{
            t.id();
            // TODO: add columns
            // t.string("name").not_null();
            t.timestamps();
        }}).await
    }}

    async fn down(&self, schema: &SchemaExecutor) -> anyhow::Result<()> {{
        schema.drop_table_if_exists("{snake}").await
    }}
}}
"#
    )
}

// ── Seeder ────────────────────────────────────────────────────────────────────

pub fn seeder(name: &str) -> anyhow::Result<()> {
    let camel = name.to_upper_camel_case();
    let base = camel.strip_suffix("Seeder").unwrap_or(&camel);
    let pascal = base.to_upper_camel_case();
    let snake = base.to_snake_case();
    let path = format!("src/database/seeders/{snake}_seeder.rs");

    fs::create_dir_all("src/database/seeders")?;
    write_file(&path, &seeder_template(&pascal))?;
    println!("Created  {path}");
    Ok(())
}

fn seeder_template(pascal: &str) -> String {
    format!(
        r#"use sqlx::PgPool;

pub struct {pascal}Seeder;

impl {pascal}Seeder {{
    pub async fn run(pool: &PgPool) -> Result<(), sqlx::Error> {{
        // TODO: insert seed data
        Ok(())
    }}
}}
"#
    )
}

// ── Validator ────────────────────────────────────────────────────────────────

pub fn validator(name: &str) -> anyhow::Result<()> {
    let camel = name.to_upper_camel_case();
    let base = camel.strip_suffix("Request").unwrap_or(&camel);
    let pascal = base.to_upper_camel_case();
    let snake = base.to_snake_case();
    let path = format!("src/app/validators/{snake}_request.rs");

    write_file(&path, &validator_template(&pascal))?;
    println!("Created  {path}");
    println!("Register it in src/app/validators/mod.rs:");
    println!("  pub mod {snake}_request;");
    Ok(())
}

fn validator_template(pascal: &str) -> String {
    format!(
        r#"use rok_validate::Validate;
use serde::Deserialize;

#[derive(Deserialize, Validate)]
pub struct {pascal}Request {{
    // TODO: add validated fields
    // #[validate(required, min = 1, max = 255)]
    // pub name: String,
}}
"#
    )
}

// ── Request ───────────────────────────────────────────────────────────────────

/// Generate a single request file in `src/app/requests/`.
pub fn request(name: &str) -> anyhow::Result<()> {
    let camel = name.to_upper_camel_case();
    let base = camel.strip_suffix("Request").unwrap_or(&camel);
    let pascal = base.to_upper_camel_case();
    let snake = base.to_snake_case();
    let path = format!("src/app/requests/{snake}_request.rs");
    write_file(&path, &request_template(&pascal))?;
    println!("Created  {path}");
    println!("Register it in src/app/requests/mod.rs:");
    println!("  pub mod {snake}_request;");
    Ok(())
}

/// Generate a create+update request pair in `src/app/requests/`.
pub fn make_request_pair(name: &str) -> anyhow::Result<()> {
    let pascal = name.to_upper_camel_case();
    let snake = name.to_snake_case();

    let create_path = format!("src/app/requests/create_{snake}_request.rs");
    write_file(&create_path, &request_template(&format!("Create{pascal}")))?;
    println!("Created  {create_path}");

    let update_path = format!("src/app/requests/update_{snake}_request.rs");
    write_file(&update_path, &request_template(&format!("Update{pascal}")))?;
    println!("Created  {update_path}");
    Ok(())
}

fn request_template(pascal: &str) -> String {
    format!(
        r#"use rok_validate::{{FormRequest, Validate}};
use serde::Deserialize;

#[derive(Deserialize, Validate, FormRequest)]
pub struct {pascal}Request {{
    // TODO: add validated fields
    // #[validate(required, bail, min = 1, max = 255)]
    // pub name: String,

    // #[validate(sometimes, required, email)]
    // pub email: Option<String>,
}}
"#
    )
}

// ── Service ───────────────────────────────────────────────────────────────────

pub fn make_service(name: &str, force: bool) -> anyhow::Result<()> {
    let pascal = name.to_upper_camel_case();
    let snake = name.to_snake_case();
    let path = format!("src/app/services/{snake}_service.rs");
    write_file_guarded(&path, &service_template(&pascal, &snake), force)?;
    println!("Created  {path}");
    println!("Register in src/app/services/mod.rs:");
    println!("  pub mod {snake}_service;");
    Ok(())
}

fn service_template(pascal: &str, snake: &str) -> String {
    format!(
        r#"use sqlx::PgPool;

use crate::app::models::{snake}::{pascal};

pub struct {pascal}Service<'a> {{
    pool: &'a PgPool,
}}

impl<'a> {pascal}Service<'a> {{
    pub fn new(pool: &'a PgPool) -> Self {{
        Self {{ pool }}
    }}

    pub async fn all(&self) -> Result<Vec<{pascal}>, sqlx::Error> {{
        {pascal}::all(self.pool).await
    }}

    pub async fn find(&self, id: i64) -> Result<{pascal}, sqlx::Error> {{
        {pascal}::find_or_fail(self.pool, id).await
    }}
}}
"#
    )
}

// ── Locale ────────────────────────────────────────────────────────────────────

pub fn locale(lang: &str, force: bool) -> anyhow::Result<()> {
    if lang == "en" {
        anyhow::bail!("Cannot generate a template from itself. Use a language other than 'en'.");
    }
    let lang = lang.to_lowercase();
    let out_path = format!("locales/{lang}.json");

    if !force && std::path::Path::new(&out_path).exists() {
        anyhow::bail!("{out_path} already exists. Pass --force to overwrite.");
    }

    // Read locales/en.json as template; fall back to a minimal built-in template.
    let template_map: serde_json::Map<String, serde_json::Value> =
        if let Ok(content) = std::fs::read_to_string("locales/en.json") {
            serde_json::from_str(&content).unwrap_or_else(|_| default_locale_template())
        } else {
            default_locale_template()
        };

    // Replace all string values with "" so the translator fills them in.
    let blank = blank_locale_map(&template_map);
    let json = serde_json::to_string_pretty(&blank)?;

    fs::create_dir_all("locales")?;
    fs::write(&out_path, json + "\n")?;
    println!("Created  {out_path}");
    println!("Translate the values in {out_path} then add I18nLayer::with_dir(\"locales\", \"{lang}\") to your router.");
    Ok(())
}

fn blank_locale_map(
    template: &serde_json::Map<String, serde_json::Value>,
) -> serde_json::Map<String, serde_json::Value> {
    use serde_json::Value;
    let mut out = serde_json::Map::new();
    for (k, v) in template {
        let blank_val = match v {
            Value::String(_) => Value::String(String::new()),
            Value::Object(inner) => {
                let mut obj = serde_json::Map::new();
                for (ik, iv) in inner {
                    obj.insert(
                        ik.clone(),
                        if iv.is_string() {
                            Value::String(String::new())
                        } else {
                            iv.clone()
                        },
                    );
                }
                Value::Object(obj)
            }
            other => other.clone(),
        };
        out.insert(k.clone(), blank_val);
    }
    out
}

fn default_locale_template() -> serde_json::Map<String, serde_json::Value> {
    let v: serde_json::Value = serde_json::json!({
        "validation.required":   "The {field} field is required.",
        "validation.email":      "The {field} field must be a valid email address.",
        "validation.url":        "The {field} field must be a valid URL.",
        "validation.numeric":    "The {field} field must be a number.",
        "validation.min_length": "The {field} field must be at least {min} character(s).",
        "validation.max_length": "The {field} field may not be greater than {max} character(s).",
        "validation.min":        "The {field} field must be at least {min}.",
        "validation.max":        "The {field} field may not be greater than {max}.",
        "validation.same":       "The {field} field confirmation does not match.",
        "validation.in":         "The {field} field must be one of: {options}."
    });
    v.as_object().cloned().unwrap_or_default()
}

// ── Observer ──────────────────────────────────────────────────────────────────

pub fn observer(name: &str, force: bool) -> anyhow::Result<()> {
    let pascal = name.to_upper_camel_case();
    let snake = name.to_snake_case();
    let path = format!("src/app/observers/{snake}.rs");
    write_file_guarded(&path, &observer_template(&pascal), force)?;
    println!("Created  {path}");
    println!("Register with: rok_orm::observe::<Model, {pascal}>();");
    Ok(())
}

fn observer_template(pascal: &str) -> String {
    format!(
        r#"use rok_orm::{{Observer, OrmResult}};

pub struct {pascal};

impl<M: rok_orm::Model> Observer<M> for {pascal} {{
    fn creating(&self, _model: &M) -> OrmResult<()> {{ Ok(()) }}
    fn created(&self, _model: &M) {{}}
    fn updating(&self, _model: &M) -> OrmResult<()> {{ Ok(()) }}
    fn updated(&self, _model: &M) {{}}
    fn deleting(&self, _model: &M) -> OrmResult<()> {{ Ok(()) }}
    fn deleted(&self, _model: &M) {{}}
}}
"#
    )
}

// ── Resource ──────────────────────────────────────────────────────────────────

pub fn resource(name: &str, force: bool) -> anyhow::Result<()> {
    let pascal = name.to_upper_camel_case();
    let snake = name.to_snake_case();
    let path = format!("src/app/resources/{snake}_resource.rs");
    write_file_guarded(&path, &resource_template(&pascal), force)?;
    println!("Created  {path}");
    println!("Register in src/app/resources/mod.rs:");
    println!("  pub mod {snake}_resource;");
    Ok(())
}

fn resource_template(pascal: &str) -> String {
    format!(
        r#"use serde_json::json;

pub struct {pascal}Resource;

impl {pascal}Resource {{
    pub fn transform(model: &impl serde::Serialize) -> serde_json::Value {{
        // TODO: map model fields to the response shape
        json!({{ "id": 0 }})
    }}
}}
"#
    )
}

// ── Scope ─────────────────────────────────────────────────────────────────────

pub fn scope(name: &str, force: bool) -> anyhow::Result<()> {
    let pascal = name.to_upper_camel_case();
    let snake = name.to_snake_case();
    let path = format!("src/app/scopes/{snake}.rs");
    write_file_guarded(&path, &scope_template(&pascal), force)?;
    println!("Created  {path}");
    Ok(())
}

fn scope_template(pascal: &str) -> String {
    format!(
        r#"use rok_orm::{{Model, QueryBuilder}};

pub struct {pascal};

impl {pascal} {{
    pub fn apply<M: Model>(builder: QueryBuilder<M>) -> QueryBuilder<M> {{
        // TODO: add scope conditions
        // builder.where_eq("active", true)
        builder
    }}
}}
"#
    )
}

// ── Policy ────────────────────────────────────────────────────────────────────

pub fn policy(name: &str, force: bool) -> anyhow::Result<()> {
    let pascal = name.to_upper_camel_case();
    let snake = name.to_snake_case();
    let path = format!("src/app/policies/{snake}.rs");
    write_file_guarded(&path, &policy_template(&pascal), force)?;
    println!("Created  {path}");
    println!("Register in src/app/policies/mod.rs:");
    println!("  pub mod {snake};");
    Ok(())
}

fn policy_template(pascal: &str) -> String {
    format!(
        r#"pub struct {pascal};

impl {pascal} {{
    pub fn view(&self, _user_id: i64) -> bool {{ true }}
    pub fn create(&self, _user_id: i64) -> bool {{ true }}
    pub fn update(&self, _user_id: i64) -> bool {{ true }}
    pub fn delete(&self, _user_id: i64) -> bool {{ true }}
}}
"#
    )
}

// ── Job ───────────────────────────────────────────────────────────────────────

pub fn job(name: &str, force: bool) -> anyhow::Result<()> {
    let pascal = name.to_upper_camel_case();
    let snake = name.to_snake_case();
    let path = format!("src/app/jobs/{snake}.rs");
    write_file_guarded(&path, &job_template(&pascal), force)?;
    println!("Created  {path}");
    println!("Dispatch with: rok_queue::dispatch({pascal} {{ ... }}).await?;");
    Ok(())
}

fn job_template(pascal: &str) -> String {
    format!(
        r#"use rok_queue::{{Job, JobContext, JobError, Runnable}};
use serde::{{Deserialize, Serialize}};

#[derive(Serialize, Deserialize, Job)]
#[job(queue = "default", max_attempts = 3)]
pub struct {pascal} {{
    // TODO: add job payload fields
    pub id: i64,
}}

impl Runnable for {pascal} {{
    async fn handle(&self, _ctx: &JobContext) -> Result<(), JobError> {{
        // TODO: implement job logic
        Ok(())
    }}
}}
"#
    )
}

// ── Event ─────────────────────────────────────────────────────────────────────

pub fn event(name: &str, force: bool) -> anyhow::Result<()> {
    let pascal = name.to_upper_camel_case();
    let snake = name.to_snake_case();
    let path = format!("src/app/events/{snake}.rs");
    write_file_guarded(&path, &event_template(&pascal), force)?;
    println!("Created  {path}");
    println!("Emit with: rok_events::emit({pascal} {{ ... }});");
    Ok(())
}

fn event_template(pascal: &str) -> String {
    format!(
        r#"use rok_events::Event;
use serde::{{Deserialize, Serialize}};

#[derive(Serialize, Deserialize, Event)]
pub struct {pascal} {{
    // TODO: add event fields
}}
"#
    )
}

// ── Listener ──────────────────────────────────────────────────────────────────

pub fn listener(name: &str, force: bool) -> anyhow::Result<()> {
    let pascal = name.to_upper_camel_case();
    let snake = name.to_snake_case();
    let path = format!("src/app/listeners/{snake}.rs");
    write_file_guarded(&path, &listener_template(&pascal), force)?;
    println!("Created  {path}");
    println!("Register with: rok_events::listen::<EventType, {pascal}>();");
    Ok(())
}

fn listener_template(pascal: &str) -> String {
    format!(
        r#"pub struct {pascal};

impl {pascal} {{
    pub async fn handle<E: rok_events::Event>(&self, _event: &E) {{
        // TODO: implement listener logic
    }}
}}
"#
    )
}

// ── Notification ──────────────────────────────────────────────────────────────

pub fn notification(name: &str, force: bool) -> anyhow::Result<()> {
    let pascal = name.to_upper_camel_case();
    let snake = name.to_snake_case();
    let path = format!("src/app/notifications/{snake}.rs");
    write_file_guarded(&path, &notification_template(&pascal), force)?;
    println!("Created  {path}");
    Ok(())
}

fn notification_template(pascal: &str) -> String {
    format!(
        r#"use serde::{{Deserialize, Serialize}};

#[derive(Serialize, Deserialize)]
pub struct {pascal} {{
    // TODO: add notification fields
}}

impl {pascal} {{
    pub fn channels(&self) -> Vec<&'static str> {{
        vec!["mail", "database"]
    }}
}}
"#
    )
}

// ── Test ──────────────────────────────────────────────────────────────────────

pub fn test(name: &str, force: bool) -> anyhow::Result<()> {
    let pascal = name.to_upper_camel_case();
    let snake = name.to_snake_case();
    let crud_path = format!("tests/{snake}_test.rs");
    if !force && Path::new(&crud_path).exists() {
        println!("skip  {crud_path} already exists — use --force to create a second file");
        return Ok(());
    }
    let path = format!("tests/{snake}.rs");
    write_file_guarded(&path, &test_template(&pascal, &snake), force)?;
    println!("Created  {path}");
    Ok(())
}

fn test_template(pascal: &str, snake: &str) -> String {
    let _ = pascal;
    format!(
        r#"#[cfg(test)]
mod tests {{
    #[tokio::test]
    async fn {snake}_test() {{
        // TODO: implement test
    }}
}}
"#
    )
}

// ── make:auth ─────────────────────────────────────────────────────────────────

/// Generate a complete authentication system.
pub fn auth(guard: &str, features: &str, model: &str, force: bool) -> anyhow::Result<()> {
    let model_pascal = model.to_upper_camel_case();
    let model_snake = model.to_snake_case();
    let is_jwt = guard == "jwt";
    let has_email_verify = features.contains("email-verify");
    let has_password_reset = features.contains("password-reset");
    let has_totp = features.contains("totp");

    let password_reset_imports = if has_password_reset {
        "use crate::app::validators::forgot_password_request::ForgotPasswordRequest;\nuse crate::app::validators::reset_password_request::ResetPasswordRequest;"
    } else {
        ""
    };

    let features_doc = if has_email_verify && has_password_reset {
        "email-verify, password-reset"
    } else if has_email_verify {
        "email-verify"
    } else if has_password_reset {
        "password-reset"
    } else {
        ""
    };

    // User model
    {
        let path = format!("src/app/models/{model_snake}.rs");
        write_file_guarded(&path, &auth_model_template(&model_pascal, &model_snake, is_jwt, has_email_verify), force)?;
        println!("Created  {path}");
    }

    // Migration
    {
        use chrono::Local;
        let ts = Local::now().format("%Y_%m_%d_%H%M%S").to_string();
        let dir = migration_dir();
        fs::create_dir_all(dir)?;
        let filename = format!("{dir}/{ts}_create_{model_snake}s_table.rs");
        write_file_guarded(&filename, &auth_migration_template(&model_pascal, &model_snake, &ts, is_jwt, has_email_verify), force)?;
        println!("Created  {filename}");
    }

    // Auth controller
    {
        let path = "src/app/controllers/auth_controller.rs".to_string();
        write_file_guarded(&path, &auth_controller_template(is_jwt, has_email_verify, has_password_reset, has_totp, features_doc, password_reset_imports), force)?;
        println!("Created  {path}");
    }

    // Validators
    {
        let path = "src/app/validators/login_request.rs".to_string();
        write_file_guarded(&path, &login_validator_template(), force)?;
        println!("Created  {path}");
    }
    {
        let path = "src/app/validators/register_request.rs".to_string();
        write_file_guarded(&path, &register_validator_template(), force)?;
        println!("Created  {path}");
    }
    if has_password_reset {
        let path = "src/app/validators/forgot_password_request.rs".to_string();
        write_file_guarded(&path, &forgot_password_validator_template(), force)?;
        println!("Created  {path}");
    }
    if has_password_reset {
        let path = "src/app/validators/reset_password_request.rs".to_string();
        write_file_guarded(&path, &reset_password_validator_template(), force)?;
        println!("Created  {path}");
    }

    // Factory
    {
        fs::create_dir_all("src/database/factories")?;
        let path = format!("src/database/factories/{model_snake}_factory.rs");
        write_file_guarded(&path, &auth_factory_template(&model_pascal), force)?;
        println!("Created  {path}");
    }

    // Seeder
    {
        fs::create_dir_all("src/database/seeders")?;
        let path = format!("src/database/seeders/{model_snake}_seeder.rs");
        write_file_guarded(&path, &auth_seeder_template(&model_pascal), force)?;
        println!("Created  {path}");
    }

    println!();
    println!("Next steps:");
    println!("  1. Register auth routes:");
    println!("     use crate::app::controllers::auth_controller::AuthController;");
    println!("     .nest(\"/auth\", AuthController::routes())");
    if is_jwt {
        println!("  2. Add JWT_SECRET to your .env file");
    }
    if has_email_verify {
        println!("  3. Configure mail for email verification");
    }
    println!("  4. Register {}Seeder in your seeder runner", model_pascal);
    println!("  5. Run: cargo run -- db:migrate && cargo run -- db:seed");
    Ok(())
}

fn auth_model_template(pascal: &str, _snake: &str, is_jwt: bool, has_email_verify: bool) -> String {
    let id_field = if is_jwt {
        "    pub id: i64,\n    // Migration: t.id();".to_string()
    } else {
        "    pub id: i64,\n    // Migration: t.id();".to_string()
    };
    let email_verify_fields = if has_email_verify {
        r#"    pub email_verified_at: Option<chrono::DateTime<chrono::Utc>>,"#.to_string()
    } else {
        String::new()
    };
    format!(
        r#"use rok_orm::Model;
use serde::{{Deserialize, Serialize}};

#[derive(Debug, Clone, Model, Serialize, Deserialize, sqlx::FromRow)]
pub struct {pascal} {{
{id_field}
    pub name: String,
    pub email: String,
    pub password: String,
{email_verify_fields}
    pub remember_token: Option<String>,
    pub created_at: chrono::DateTime<chrono::Utc>,
    pub updated_at: chrono::DateTime<chrono::Utc>,
}}
"#,
        id_field = id_field,
        email_verify_fields = email_verify_fields
    )
}

fn auth_migration_template(pascal: &str, snake: &str, timestamp: &str, _is_jwt: bool, has_email_verify: bool) -> String {
    let _ = pascal;
    let email_verify_col = if has_email_verify {
        "            t.timestamp_nullable(\"email_verified_at\");"
    } else {
        ""
    };
    format!(
        r#"use rok_orm::{{Migration, SchemaExecutor}};
use async_trait::async_trait;

pub struct Create{pascal}sTable;

#[async_trait]
impl Migration for Create{pascal}sTable {{
    fn name(&self) -> &str {{
        "{timestamp}_create_{snake}s_table"
    }}

    async fn up(&self, schema: &SchemaExecutor) -> anyhow::Result<()> {{
        schema.create("{snake}s", |t| {{
            t.id();
            t.string("name").not_null();
            t.string("email").unique().not_null();
            t.string("password").not_null();
{email_verify_col}
            t.string_nullable("remember_token");
            t.timestamps();
        }}).await
    }}

    async fn down(&self, schema: &SchemaExecutor) -> anyhow::Result<()> {{
        schema.drop_table_if_exists("{snake}s").await
    }}
}}
"#,
        email_verify_col = email_verify_col
    )
}

fn auth_controller_template(
    is_jwt: bool,
    has_email_verify: bool,
    has_password_reset: bool,
    _has_totp: bool,
    _features_doc: &str,
    password_reset_imports: &str,
) -> String {
    let auth_header = if is_jwt {
        r#"use rok_auth::jwt::{JwtToken, TokenResponse};
use rok_auth::Hasher;"#.to_string()
    } else {
        r#"use rok_auth::session::SessionUser;
use rok_auth::Hasher;"#.to_string()
    };

    let login_return = if is_jwt {
        r#"        let token = JwtToken::from_user(&user).map_err(|e| Response::internal_error(e.to_string()))?;
        Response::json(serde_json::json!({ "token": token.access_token(), "user": user }))"#.to_string()
    } else {
        r#"        SessionUser::login(&ctx, &user).map_err(|e| Response::internal_error(e.to_string()))?;
        Response::json(serde_json::json!({ "user": user }))"#.to_string()
    };

    let logout_handler = if is_jwt {
        r#"
    /// Invalidate the current token.
    pub async fn logout(ctx: Ctx) -> impl IntoResponse {
        JwtToken::from_ctx(&ctx)?.revoke().await;
        Response::no_content()
    }
"#.to_string()
    } else {
        r#"
    /// End the current session.
    pub async fn logout(ctx: Ctx) -> impl IntoResponse {
        SessionUser::logout(&ctx);
        Response::no_content()
    }
"#.to_string()
    };

    let email_verify_handler = if has_email_verify {
        r#"
    /// Send email verification link.
    pub async fn send_verify_email(ctx: Ctx) -> impl IntoResponse {
        let user = ctx.user()?;
        // TODO: dispatch SendEmailVerification notification
        Response::json(serde_json::json!({ "message": "Verification email sent" }))
    }

    /// Verify email with signed URL token.
    pub async fn verify_email(ctx: Ctx, Path(token): Path<String>) -> impl IntoResponse {
        // TODO: validate signed token, mark email_verified_at
        Response::json(serde_json::json!({ "message": "Email verified" }))
    }
"#.to_string()
    } else {
        String::new()
    };

    let password_reset_handler = if has_password_reset {
        r#"
    /// Send password reset link.
    pub async fn forgot_password(
        Valid(Json(req)): Valid<Json<ForgotPasswordRequest>>,
    ) -> impl IntoResponse {
        // TODO: dispatch SendPasswordReset notification
        Response::json(serde_json::json!({ "message": "Reset link sent if email exists" }))
    }

    /// Reset password with signed token.
    pub async fn reset_password(
        Valid(Json(req)): Valid<Json<ResetPasswordRequest>>,
    ) -> impl IntoResponse {
        // TODO: validate token, update password
        Response::json(serde_json::json!({ "message": "Password updated" }))
    }
"#.to_string()
    } else {
        String::new()
    };

    format!(
        r#"use axum::{{extract::Path, response::IntoResponse, Json}};
use rok_auth::axum::{{Ctx, Response}};
use rok_auth_macros::controller;
use rok_validate::Valid;
use serde::Deserialize;

{auth_header}

use crate::app::models::User;
use crate::app::validators::login_request::LoginRequest;
use crate::app::validators::register_request::RegisterRequest;
{password_reset_imports}

#[controller]
pub struct AuthController;

impl AuthController {{
    /// Register a new user.
    pub async fn register(
        Valid(Json(req)): Valid<Json<RegisterRequest>>,
    ) -> impl IntoResponse {{
        let hash = Hasher::make(&req.password);
        let user = User::create(&rok_orm::pool(), serde_json::json!({{
            "name": req.name,
            "email": req.email,
            "password": hash,
        }}))
        .await
        .map_err(|e| Response::internal_error(e.to_string()))?;

        Response::json(serde_json::json!({{ "message": "User registered", "user": user }}))
    }}

    /// Authenticate and return a token / session.
    pub async fn login(
        Valid(Json(req)): Valid<Json<LoginRequest>>,
    ) -> impl IntoResponse {{
        let user = User::find_by("email", &req.email)
            .await
            .map_err(|_| Response::unauthorized("Invalid credentials"))?
            .ok_or_else(|| Response::unauthorized("Invalid credentials"))?;

        if !Hasher::check(&req.password, &user.password) {{
            return Err(Response::unauthorized("Invalid credentials"));
        }}

{login_return}
    }}

{logout_handler}
{email_verify_handler}
{password_reset_handler}

    /// Get the current authenticated user.
    pub async fn me(ctx: Ctx) -> impl IntoResponse {{
        let user = ctx.user::<User>()?;
        Response::json(serde_json::json!({{ "user": user }}))
    }}
}}
"#,
        auth_header = auth_header,
        login_return = login_return,
        logout_handler = logout_handler,
        email_verify_handler = email_verify_handler,
        password_reset_handler = password_reset_handler,
    )
}

fn login_validator_template() -> String {
    r#"use rok_validate::Validate;
use serde::Deserialize;

#[derive(Deserialize, Validate)]
pub struct LoginRequest {
    #[validate(required, email)]
    pub email: String,
    #[validate(required, min = 8)]
    pub password: String,
}
"#
    .to_string()
}

fn register_validator_template() -> String {
    r#"use rok_validate::Validate;
use serde::Deserialize;

#[derive(Deserialize, Validate)]
pub struct RegisterRequest {
    #[validate(required, min = 2, max = 255)]
    pub name: String,
    #[validate(required, email)]
    pub email: String,
    #[validate(required, min = 8, confirmed)]
    pub password: String,
    #[validate(required, same = "password")]
    pub password_confirmation: String,
}
"#
    .to_string()
}

fn forgot_password_validator_template() -> String {
    r#"use rok_validate::Validate;
use serde::Deserialize;

#[derive(Deserialize, Validate)]
pub struct ForgotPasswordRequest {
    #[validate(required, email)]
    pub email: String,
}
"#
    .to_string()
}

fn reset_password_validator_template() -> String {
    r#"use rok_validate::Validate;
use serde::Deserialize;

#[derive(Deserialize, Validate)]
pub struct ResetPasswordRequest {
    #[validate(required)]
    pub token: String,
    #[validate(required, email)]
    pub email: String,
    #[validate(required, min = 8, confirmed)]
    pub password: String,
    #[validate(required, same = "password")]
    pub password_confirmation: String,
}
"#
    .to_string()
}

fn auth_factory_template(pascal: &str) -> String {
    format!(
        r#"use rok_orm::factory::Factory;
use rok_core::crypto::hasher::Hasher;

pub struct {pascal}Factory;

impl Factory for {pascal}Factory {{
    type Model = super::{pascal};

    fn definition() -> Vec<(&'static str, rok_orm::SqlValue)> {{
        vec![
            ("name", "Test User".into()),
            ("email", "test@example.com".into()),
            ("password", Hasher::make("password").into()),
        ]
    }}
}}
"#
    )
}

fn auth_seeder_template(pascal: &str) -> String {
    format!(
        r#"use sqlx::PgPool;
use {pascal}Factory;

pub struct {pascal}Seeder;

impl {pascal}Seeder {{
    pub async fn run(pool: &PgPool) -> Result<(), sqlx::Error> {{
        {pascal}Factory::create(pool, None).await?;
        println!("Created default {pascal}");
        Ok(())
    }}
}}
"#
    )
}

// ── helpers ───────────────────────────────────────────────────────────────────

fn write_file(path: &str, content: &str) -> anyhow::Result<()> {
    write_file_guarded(path, content, false)
}

fn write_file_guarded(path: &str, content: &str, force: bool) -> anyhow::Result<()> {
    let p = Path::new(path);
    if !force && p.exists() {
        anyhow::bail!("{path} already exists. Pass --force to overwrite.");
    }
    if let Some(parent) = p.parent() {
        fs::create_dir_all(parent)?;
    }
    fs::write(p, content)?;
    Ok(())
}
