//! `--template api` — REST API with JWT auth, CRUD-ready scaffold.

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

pub fn generate(name: &str, root: &Path) -> anyhow::Result<()> {
    for dir in &[
        "src/app/controllers",
        "src/app/models",
        "src/app/validators",
        "src/app/middleware",
        "src/app/events",
        "src/app/services",
        "src/app/policies",
        "src/config",
        "src/routes",
        "database/migrations",
        "tests/common",
    ] {
        fs::create_dir_all(root.join(dir))?;
    }

    let files: Vec<(&str, String)> = vec![
        ("Cargo.toml", cargo_toml(name)),
        (".env.example", ENV_EXAMPLE.into()),
        (".gitignore", GITIGNORE.into()),
        ("src/main.rs", MAIN_RS.into()),
        ("src/lib.rs", LIB_RS.into()),
        ("src/state.rs", STATE_RS.into()),
        ("src/routes/mod.rs", ROUTES_MOD_RS.into()),
        ("src/routes/api.rs", ROUTES_API_RS.into()),
        ("src/routes/auth.rs", ROUTES_AUTH_RS.into()),
        ("src/config/mod.rs", CONFIG_MOD_RS.into()),
        ("src/config/app.rs", CONFIG_APP_RS.into()),
        ("src/config/auth.rs", CONFIG_AUTH_RS.into()),
        ("src/config/database.rs", CONFIG_DB_RS.into()),
        ("src/app/mod.rs", APP_MOD_RS.into()),
        ("src/app/middleware/mod.rs", MIDDLEWARE_MOD_RS.into()),
        ("src/app/middleware/auth.rs", MIDDLEWARE_AUTH_RS.into()),
        ("src/app/middleware/logger.rs", MIDDLEWARE_LOGGER_RS.into()),
        ("src/app/middleware/cors.rs", MIDDLEWARE_CORS_RS.into()),
        ("src/app/controllers/mod.rs", CONTROLLERS_MOD_RS.into()),
        (
            "src/app/controllers/auth_controller.rs",
            AUTH_CONTROLLER_RS.into(),
        ),
        (
            "src/app/controllers/user_controller.rs",
            USER_CONTROLLER_RS.into(),
        ),
        ("src/app/models/mod.rs", MODELS_MOD_RS.into()),
        ("src/app/models/user.rs", USER_MODEL_RS.into()),
        ("src/app/validators/mod.rs", VALIDATORS_MOD_RS.into()),
        (
            "src/app/validators/auth_requests.rs",
            AUTH_REQUESTS_RS.into(),
        ),
        ("src/app/policies/mod.rs", POLICIES_MOD_RS.into()),
        ("src/app/events/mod.rs", EVENTS_MOD_RS.into()),
        ("src/app/services/mod.rs", SERVICES_MOD_RS.into()),
        ("database/migrations/001_users.sql", MIGRATION_USERS.into()),
        (
            "database/migrations/002_tokens.sql",
            MIGRATION_TOKENS.into(),
        ),
        ("tests/common/mod.rs", TESTS_COMMON_RS.into()),
        ("tests/auth.rs", TESTS_AUTH_RS.into()),
    ];

    for (rel, content) in &files {
        fs::write(root.join(rel), content)?;
        println!("  create  {rel}");
    }

    Ok(())
}

fn cargo_toml(name: &str) -> String {
    // Use only the final path component as the package name so that paths like
    // `examples/my-app` produce `name = "my-app"` rather than the invalid
    // `name = "examples/my-app"`.
    let pkg_name = std::path::Path::new(name)
        .file_name()
        .and_then(|n| n.to_str())
        .unwrap_or(name);
    format!(
        r#"[package]
name = "{pkg_name}"
version = "0.1.0"
edition = "2024"

[[bin]]
name = "{pkg_name}"
path = "src/main.rs"

[dependencies]
axum         = "0.8"
tokio        = {{ version = "1", features = ["full"] }}
tower-http   = {{ version = "0.6", features = ["trace", "cors"] }}
serde        = {{ version = "1", features = ["derive"] }}
serde_json   = "1"
sqlx         = {{ version = "0.8", default-features = false, features = [
    "macros", "runtime-tokio-native-tls", "chrono", "postgres",
] }}
chrono       = {{ version = "0.4", features = ["serde"] }}
dotenvy      = "0.15"
anyhow       = "1"
thiserror    = "2"
rok-auth     = {{ version = "0.1", features = ["axum"] }}
rok-orm      = {{ version = "0.1", features = ["postgres", "axum"] }}
rok-validate = {{ version = "0.1", features = ["axum"] }}
rok-problem  = {{ version = "0.1", features = ["axum"] }}
rok-config   = {{ version = "0.1" }}

[dev-dependencies]
rok-testing = {{ version = "0.1" }}
tokio       = {{ version = "1" }}
"#
    )
}

const ENV_EXAMPLE: &str = r#"APP_NAME=rok-api
LISTEN_ADDR=0.0.0.0:3000
DATABASE_URL=postgres://postgres:postgres@localhost/rok_api
JWT_SECRET=change-me-in-production
MAIL_DRIVER=log
CACHE_DRIVER=memory
"#;

const GITIGNORE: &str = "/target\n.env\n.env.local\n";

const MAIN_RS: &str = r#"use axum::Router;
use sqlx::postgres::PgPoolOptions;

mod app;
mod config;
mod routes;
mod state;

use config::{AppConfig, AuthConfig, DatabaseConfig};
use routes::{api_router, auth_router};
use state::AppState;

#[tokio::main]
async fn main() {
    dotenvy::dotenv().ok();

    let app_cfg = AppConfig::load();
    let auth_cfg = AuthConfig::load();
    let db_cfg = DatabaseConfig::load();

    let pool = PgPoolOptions::new()
        .max_connections(db_cfg.max_connections)
        .connect(&db_cfg.url)
        .await
        .expect("failed to connect to database");

    let state = AppState::new(pool, auth_cfg.jwt_secret);

    let app = Router::new()
        .merge(auth_router())
        .merge(api_router())
        .with_state(state);

    let listener = tokio::net::TcpListener::bind(&app_cfg.listen_addr)
        .await
        .expect("failed to bind");

    println!("{} listening on {}", app_cfg.name, app_cfg.listen_addr);
    axum::serve(listener, app).await.expect("server error");
}
"#;

const LIB_RS: &str = r#"pub mod app;
pub mod config;
pub mod routes;
pub mod state;
"#;

const STATE_RS: &str = r#"use std::sync::Arc;
use rok_auth::{Auth, AuthConfig};
use rok_auth::axum::{HasAuth, HasPool};
use sqlx::PgPool;

#[derive(Clone)]
pub struct AppState {
    pub pool: PgPool,
    pub auth: Auth,
}

impl AppState {
    pub fn new(pool: PgPool, jwt_secret: String) -> Self {
        let auth = Auth::new(AuthConfig {
            secret: jwt_secret,
            ..Default::default()
        });
        Self { pool, auth }
    }
}

impl HasPool for AppState {
    fn pool(&self) -> &PgPool { &self.pool }
}

impl HasAuth for AppState {
    fn auth_handle(&self) -> Arc<Auth> { Arc::new(self.auth.clone()) }
}
"#;

const ROUTES_MOD_RS: &str = r#"pub mod api;
pub mod auth;

pub use api::api_router;
pub use auth::auth_router;
"#;

const ROUTES_API_RS: &str = r#"use axum::{routing::get, Router};
use crate::app::controllers::user_controller::UserController;
use crate::state::AppState;

pub fn api_router() -> Router<AppState> {
    Router::new()
        .route("/api/v1/users",     get(UserController::index))
        .route("/api/v1/users/:id", get(UserController::show))
}
"#;

const ROUTES_AUTH_RS: &str = r#"use axum::{routing::{get, post}, Router};
use crate::app::controllers::auth_controller::AuthController;
use crate::state::AppState;

pub fn auth_router() -> Router<AppState> {
    Router::new()
        .route("/auth/register", post(AuthController::register))
        .route("/auth/login",    post(AuthController::login))
        .route("/auth/logout",   post(AuthController::logout))
        .route("/me",            get(AuthController::me))
}
"#;

const CONFIG_MOD_RS: &str = r#"pub mod app;
pub mod auth;
pub mod database;

pub use app::AppConfig;
pub use auth::AuthConfig;
pub use database::DatabaseConfig;
"#;

const CONFIG_APP_RS: &str = r#"use rok_config::Config;

#[derive(Config, Debug)]
pub struct AppConfig {
    #[env("APP_NAME", default = "rok-api")]
    pub name: String,
    #[env("LISTEN_ADDR", default = "0.0.0.0:3000")]
    pub listen_addr: String,
}
"#;

const CONFIG_AUTH_RS: &str = r#"use rok_config::Config;

#[derive(Config, Debug)]
pub struct AuthConfig {
    #[env("JWT_SECRET")]
    pub jwt_secret: String,
}
"#;

const CONFIG_DB_RS: &str = r#"use rok_config::Config;

#[derive(Config, Debug)]
pub struct DatabaseConfig {
    #[env("DATABASE_URL")]
    pub url: String,
    #[env("DB_MAX_CONNECTIONS", default = 10)]
    pub max_connections: u32,
}
"#;

const APP_MOD_RS: &str = r#"pub mod controllers;
pub mod events;
pub mod middleware;
pub mod models;
pub mod policies;
pub mod services;
pub mod validators;
"#;

const MIDDLEWARE_MOD_RS: &str = "pub mod auth;\npub mod cors;\npub mod logger;\n";

const MIDDLEWARE_AUTH_RS: &str = r#"use rok_auth::Auth;
use rok_auth::axum::AuthLayer;

pub fn layer(auth: Auth) -> AuthLayer {
    AuthLayer::new(auth)
}
"#;

const MIDDLEWARE_LOGGER_RS: &str = r#"use tower_http::trace::TraceLayer;

pub fn layer() -> TraceLayer<tower_http::classify::SharedClassifier<tower_http::classify::ServerErrorsAsFailures>> {
    TraceLayer::new_for_http()
}
"#;

const MIDDLEWARE_CORS_RS: &str = r#"use tower_http::cors::CorsLayer;

pub fn layer() -> CorsLayer {
    CorsLayer::permissive()
}
"#;

const CONTROLLERS_MOD_RS: &str = "pub mod auth_controller;\npub mod user_controller;\n";

const AUTH_CONTROLLER_RS: &str = r#"use axum::{extract::State, response::IntoResponse, Json};
use rok_auth::axum::Ctx;
use serde_json::json;
use crate::state::AppState;

pub struct AuthController;

impl AuthController {
    pub async fn register(State(_state): State<AppState>) -> impl IntoResponse {
        Json(json!({ "message": "register — implement me" }))
    }

    pub async fn login(State(_state): State<AppState>) -> impl IntoResponse {
        Json(json!({ "message": "login — implement me" }))
    }

    pub async fn logout(_ctx: Ctx) -> impl IntoResponse {
        Json(json!({ "message": "logged out" }))
    }

    pub async fn me(_ctx: Ctx) -> impl IntoResponse {
        Json(json!({ "message": "me — implement me" }))
    }
}
"#;

const USER_CONTROLLER_RS: &str = r#"use axum::{extract::{Path, State}, response::IntoResponse, Json};
use serde_json::json;
use crate::state::AppState;

pub struct UserController;

impl UserController {
    pub async fn index(State(_state): State<AppState>) -> impl IntoResponse {
        Json(json!({ "data": [] }))
    }

    pub async fn show(State(_state): State<AppState>, Path(id): Path<i64>) -> impl IntoResponse {
        Json(json!({ "data": { "id": id } }))
    }
}
"#;

const MODELS_MOD_RS: &str = "pub mod user;\npub use user::User;\n";

const USER_MODEL_RS: &str = r#"use rok_orm::Model;
use serde::Serialize;

#[derive(Debug, Clone, Model, Serialize, sqlx::FromRow)]
pub struct User {
    pub id:            i64,
    pub email:         String,
    #[serde(skip)]
    pub password_hash: String,
    pub created_at:    chrono::DateTime<chrono::Utc>,
}
"#;

const VALIDATORS_MOD_RS: &str = "pub mod auth_requests;\n";

const AUTH_REQUESTS_RS: &str = r#"use rok_validate::Validate;
use serde::Deserialize;

#[derive(Deserialize, Validate)]
pub struct RegisterRequest {
    #[validate(required, email)]
    pub email: String,
    #[validate(required, min = 8, max = 128)]
    pub password: String,
    #[validate(required, same = "password")]
    pub password_confirmation: String,
}

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

const POLICIES_MOD_RS: &str = "// pub mod user_policy;\n";
const EVENTS_MOD_RS: &str = "// pub mod auth_events;\n";
const SERVICES_MOD_RS: &str = "// pub mod token_service;\n";

const MIGRATION_USERS: &str = r#"CREATE TABLE IF NOT EXISTS users (
    id            BIGSERIAL PRIMARY KEY,
    email         TEXT NOT NULL UNIQUE,
    password_hash TEXT NOT NULL,
    created_at    TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
"#;

const MIGRATION_TOKENS: &str = r#"CREATE TABLE IF NOT EXISTS personal_access_tokens (
    id         BIGSERIAL PRIMARY KEY,
    user_id    BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    token_hash TEXT NOT NULL UNIQUE,
    name       TEXT,
    expires_at TIMESTAMPTZ,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_tokens_user ON personal_access_tokens(user_id);
"#;

const TESTS_COMMON_RS: &str = r#"use axum::Router;
use rok_testing::TestClient;

pub struct TestApp {
    pub client: TestClient,
}

impl TestApp {
    pub async fn boot() -> Self {
        let _ = dotenvy::from_filename(".env.test");
        TestApp { client: TestClient::new(Router::new()) }
    }
}
"#;

const TESTS_AUTH_RS: &str = r#"mod common;

#[tokio::test]
#[ignore = "requires running database"]
async fn register_and_login() {
    let _app = common::TestApp::boot().await;
    // TODO: implement auth integration test
}
"#;
