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

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

use heck::{ToSnakeCase, ToUpperCamelCase};

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

pub fn controller(name: &str) -> 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");

    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, response::IntoResponse}};
use rok_auth::axum::{{Ctx, Response}};
use rok_auth_macros::controller;

#[controller]
pub struct {pascal}Controller;

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

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

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

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

    /// `DELETE /{snake}s/:id`
    pub async fn destroy(ctx: Ctx, Path(id): Path<i64>) -> impl IntoResponse {{
        Response::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)?;
    }
    if with_resource {
        resource(name, false)?;
    }
    if with_factory {
        make_factory(name)?;
    }
    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_ids::Ulid;\n",
            "Ulid",
            "#[rok_orm(id = \"ulid\")]\n",
        ),
        Some("cuid2") => (
            "use rok_ids::Cuid2;\n",
            "Cuid2",
            "#[rok_orm(id = \"cuid2\")]\n",
        ),
        Some("uuid_v7") => (
            "use rok_ids::UuidV7;\n",
            "UuidV7",
            "#[rok_orm(id = \"uuid_v7\")]\n",
        ),
        Some("snowflake") => (
            "use rok_ids::Snowflake;\n",
            "Snowflake",
            "#[rok_orm(id = \"snowflake\")]\n",
        ),
        Some("nanoid") => (
            "use rok_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"
    }
}

fn next_migration_index(dir: &str) -> u32 {
    fs::read_dir(dir)
        .map(|rd| {
            rd.filter_map(|e| e.ok())
                .filter(|e| e.path().extension().and_then(|x| x.to_str()) == Some("sql"))
                .count() as u32
                + 1
        })
        .unwrap_or(1)
}

pub fn migration(name: &str) -> anyhow::Result<()> {
    let snake = name.to_snake_case();
    let dir = migration_dir();
    fs::create_dir_all(dir)?;
    let idx = next_migration_index(dir);
    let filename = format!("{dir}/{idx:03}_{snake}.sql");

    write_file(
        &filename,
        &format!("-- up\n\n\n-- down\n\n"),
    )?;
    println!("Created  {filename}");
    Ok(())
}

// ── 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,
}}
"#
    )
}

// ── 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
    }}
}}
"#
    )
}

// ── 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(())
}
