//! Templates for `gize make crud`: a full, compiling vertical slice for a resource —
//! model, DTOs, SQLx repository, service, Axum handlers, routes and a typed error.
//!
//! The repository uses SQLx's runtime query API (`query_as`), not the compile-time
//! `query!` macro, so generated projects compile without a database connection (ADR-003).
//! Placeholders (`__NAME__`, `__TABLE__`, ...) are substituted via `replace` to avoid
//! escaping the many braces in the generated Rust.

use gize_core::ModelSpec;
use gize_core::naming::table_name;

/// Apply the substitutions common to every CRUD file.
fn base(template: &str, model: &ModelSpec) -> String {
    let table = table_name(&model.name);
    template
        .replace("__NAME__", &model.name)
        .replace("__TABLE__", &table)
}

/// `pub name: String,` lines for the Create/Update DTOs.
fn dto_fields(model: &ModelSpec) -> String {
    model
        .fields
        .iter()
        .map(|f| format!("    pub {}: {},", f.name, f.ty.rust_type()))
        .collect::<Vec<_>>()
        .join("\n")
}

/// `.bind(...)` lines. Copy scalars bind by value; `String` binds a clone. This keeps the
/// generated repository clippy-clean (no `clone_on_copy`).
fn create_binds(model: &ModelSpec) -> String {
    model
        .fields
        .iter()
        .map(|f| {
            if f.ty.is_copy() {
                format!("        .bind(input.{})", f.name)
            } else {
                format!("        .bind(input.{}.clone())", f.name)
            }
        })
        .collect::<Vec<_>>()
        .join("\n")
}

/// `mod.rs` for the resource.
pub fn mod_rs(model: &ModelSpec) -> String {
    base(
        r#"//! The `__TABLE__` resource, generated by `gize make crud`.

pub mod dto;
pub mod error;
pub mod handler;
pub mod model;
pub mod repository;
pub mod routes;
pub mod service;

#[cfg(test)]
mod tests;

pub use routes::routes;
"#,
        model,
    )
}

/// `dto.rs`: create/update request payloads.
pub fn dto_rs(model: &ModelSpec) -> String {
    let fields = dto_fields(model);
    base(
        r#"use serde::Deserialize;

/// Payload to create a `__NAME__`.
#[derive(Debug, Deserialize)]
pub struct Create__NAME__ {
__FIELDS__
}

/// Payload to update a `__NAME__`.
#[derive(Debug, Deserialize)]
pub struct Update__NAME__ {
__FIELDS__
}
"#,
        model,
    )
    .replace("__FIELDS__", &fields)
}

/// `error.rs`: a typed error that maps to HTTP responses.
pub fn error_rs(model: &ModelSpec) -> String {
    base(
        r#"use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};

/// Errors returned by the `__TABLE__` resource.
#[derive(Debug)]
pub enum Error {
    NotFound,
    Database(sqlx::Error),
}

impl From<sqlx::Error> for Error {
    fn from(error: sqlx::Error) -> Self {
        match error {
            sqlx::Error::RowNotFound => Error::NotFound,
            other => Error::Database(other),
        }
    }
}

impl IntoResponse for Error {
    fn into_response(self) -> Response {
        let (status, message) = match self {
            Error::NotFound => (StatusCode::NOT_FOUND, "not found".to_string()),
            Error::Database(error) => (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()),
        };
        (status, message).into_response()
    }
}
"#,
        model,
    )
}

/// `repository.rs`: SQLx persistence.
pub fn repository_rs(model: &ModelSpec) -> String {
    let columns = model
        .fields
        .iter()
        .map(|f| f.name.as_str())
        .collect::<Vec<_>>()
        .join(", ");
    let placeholders = (1..=model.fields.len())
        .map(|i| format!("${i}"))
        .collect::<Vec<_>>()
        .join(", ");
    let update_set = model
        .fields
        .iter()
        .enumerate()
        .map(|(i, f)| format!("{} = ${}", f.name, i + 1))
        .collect::<Vec<_>>()
        .join(", ");
    let id_placeholder = model.fields.len() + 1;
    let create_binds = create_binds(model);
    let update_binds = format!("{create_binds}\n        .bind(id)");

    base(
        r#"use sqlx::PgPool;

use super::dto::{Create__NAME__, Update__NAME__};
use super::model::__NAME__;

pub async fn list(pool: &PgPool) -> Result<Vec<__NAME__>, sqlx::Error> {
    sqlx::query_as::<_, __NAME__>("SELECT * FROM __TABLE__ ORDER BY created_at DESC")
        .fetch_all(pool)
        .await
}

pub async fn find(pool: &PgPool, id: uuid::Uuid) -> Result<__NAME__, sqlx::Error> {
    sqlx::query_as::<_, __NAME__>("SELECT * FROM __TABLE__ WHERE id = $1")
        .bind(id)
        .fetch_one(pool)
        .await
}

pub async fn create(pool: &PgPool, input: &Create__NAME__) -> Result<__NAME__, sqlx::Error> {
    sqlx::query_as::<_, __NAME__>(
        "INSERT INTO __TABLE__ (__COLUMNS__) VALUES (__PLACEHOLDERS__) RETURNING *",
    )
__CREATE_BINDS__
        .fetch_one(pool)
        .await
}

pub async fn update(
    pool: &PgPool,
    id: uuid::Uuid,
    input: &Update__NAME__,
) -> Result<__NAME__, sqlx::Error> {
    sqlx::query_as::<_, __NAME__>(
        "UPDATE __TABLE__ SET __UPDATE_SET__, updated_at = now() WHERE id = $__ID_PH__ RETURNING *",
    )
__UPDATE_BINDS__
        .fetch_one(pool)
        .await
}

pub async fn delete(pool: &PgPool, id: uuid::Uuid) -> Result<(), sqlx::Error> {
    let result = sqlx::query("DELETE FROM __TABLE__ WHERE id = $1")
        .bind(id)
        .execute(pool)
        .await?;
    if result.rows_affected() == 0 {
        return Err(sqlx::Error::RowNotFound);
    }
    Ok(())
}
"#,
        model,
    )
    .replace("__COLUMNS__", &columns)
    .replace("__PLACEHOLDERS__", &placeholders)
    .replace("__UPDATE_SET__", &update_set)
    .replace("__ID_PH__", &id_placeholder.to_string())
    .replace("__CREATE_BINDS__", &create_binds)
    .replace("__UPDATE_BINDS__", &update_binds)
}

/// `service.rs`: business layer that maps `sqlx::Error` to the module error.
pub fn service_rs(model: &ModelSpec) -> String {
    base(
        r#"use sqlx::PgPool;

use super::dto::{Create__NAME__, Update__NAME__};
use super::error::Error;
use super::model::__NAME__;
use super::repository;

pub async fn list(pool: &PgPool) -> Result<Vec<__NAME__>, Error> {
    repository::list(pool).await.map_err(Error::from)
}

pub async fn find(pool: &PgPool, id: uuid::Uuid) -> Result<__NAME__, Error> {
    repository::find(pool, id).await.map_err(Error::from)
}

pub async fn create(pool: &PgPool, input: &Create__NAME__) -> Result<__NAME__, Error> {
    repository::create(pool, input).await.map_err(Error::from)
}

pub async fn update(
    pool: &PgPool,
    id: uuid::Uuid,
    input: &Update__NAME__,
) -> Result<__NAME__, Error> {
    repository::update(pool, id, input).await.map_err(Error::from)
}

pub async fn delete(pool: &PgPool, id: uuid::Uuid) -> Result<(), Error> {
    repository::delete(pool, id).await.map_err(Error::from)
}
"#,
        model,
    )
}

/// `handler.rs`: Axum handlers wiring HTTP to the service.
pub fn handler_rs(model: &ModelSpec) -> String {
    base(
        r#"use axum::Json;
use axum::extract::{Path, State};
use axum::http::StatusCode;

use super::dto::{Create__NAME__, Update__NAME__};
use super::error::Error;
use super::model::__NAME__;
use super::service;
use crate::state::AppState;

pub async fn list(State(state): State<AppState>) -> Result<Json<Vec<__NAME__>>, Error> {
    let items = service::list(&state.db).await?;
    Ok(Json(items))
}

pub async fn show(
    State(state): State<AppState>,
    Path(id): Path<uuid::Uuid>,
) -> Result<Json<__NAME__>, Error> {
    let item = service::find(&state.db, id).await?;
    Ok(Json(item))
}

pub async fn create(
    State(state): State<AppState>,
    Json(input): Json<Create__NAME__>,
) -> Result<(StatusCode, Json<__NAME__>), Error> {
    let item = service::create(&state.db, &input).await?;
    Ok((StatusCode::CREATED, Json(item)))
}

pub async fn update(
    State(state): State<AppState>,
    Path(id): Path<uuid::Uuid>,
    Json(input): Json<Update__NAME__>,
) -> Result<Json<__NAME__>, Error> {
    let item = service::update(&state.db, id, &input).await?;
    Ok(Json(item))
}

pub async fn delete(
    State(state): State<AppState>,
    Path(id): Path<uuid::Uuid>,
) -> Result<StatusCode, Error> {
    service::delete(&state.db, id).await?;
    Ok(StatusCode::NO_CONTENT)
}
"#,
        model,
    )
}

/// `routes.rs`: the resource's router.
pub fn routes_rs(model: &ModelSpec) -> String {
    base(
        r#"use axum::Router;
use axum::routing::get;

use super::handler;
use crate::state::AppState;

/// Routes for the `__TABLE__` resource.
pub fn routes() -> Router<AppState> {
    Router::new()
        .route("/__TABLE__", get(handler::list).post(handler::create))
        .route(
            "/__TABLE__/:id",
            get(handler::show).put(handler::update).delete(handler::delete),
        )
}
"#,
        model,
    )
}

/// `tests.rs`: placeholder until database-backed integration tests land (`gize-testing`).
pub fn tests_rs(model: &ModelSpec) -> String {
    base(
        r#"//! Tests for the `__TABLE__` resource.
//!
//! CRUD is best exercised end-to-end against a test database; `gize-testing` will provide
//! helpers for that. This placeholder keeps the module green until then.

#[test]
fn placeholder() {}
"#,
        model,
    )
}

#[cfg(test)]
mod tests {
    use super::*;

    fn product() -> ModelSpec {
        ModelSpec::parse(
            "Product",
            &[
                "name:String".to_string(),
                "price:i32".to_string(),
                "active:bool".to_string(),
            ],
        )
        .unwrap()
    }

    #[test]
    fn dto_has_all_fields() {
        let out = dto_rs(&product());
        assert!(out.contains("pub struct CreateProduct"));
        assert!(out.contains("pub struct UpdateProduct"));
        assert!(out.contains("pub name: String,"));
        assert!(out.contains("pub price: i32,"));
        assert!(out.contains("pub active: bool,"));
    }

    #[test]
    fn repository_builds_sql() {
        let out = repository_rs(&product());
        assert!(out.contains("INSERT INTO products (name, price, active) VALUES ($1, $2, $3)"));
        assert!(
            out.contains(
                "SET name = $1, price = $2, active = $3, updated_at = now() WHERE id = $4"
            )
        );
        assert!(out.contains(".bind(input.name.clone())"));
        assert!(out.contains(".bind(id)"));
    }

    #[test]
    fn handler_and_routes_reference_resource() {
        let handler = handler_rs(&product());
        assert!(handler.contains("Json<Vec<Product>>"));
        let routes = routes_rs(&product());
        assert!(routes.contains("\"/products\""));
        assert!(routes.contains("\"/products/:id\""));
    }
}
