use std::sync::Arc;

use axum::{
    extract::State,
    response::{Html, Redirect},
    routing::{get, post},
    Form, Router,
};
use cookie_cutter::{
    builtins::StandardEscaper, include_templates, value, Templates, TemplatesBuilder,
};
use serde::Deserialize;
use tokio::{net::TcpListener, sync::RwLock};

struct AppState {
    items: RwLock<Vec<(String, bool)>>,
    templates: Templates<StandardEscaper>,
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/", get(index))
        .route("/update", post(update))
        .route("/delete", post(delete))
        .route("/add", post(add))
        .with_state(Arc::new(AppState {
            templates: include_templates!("axum-todo.cctmpl"),
            items: RwLock::new(vec![
                ("Write documentation".to_string(), true),
                ("Publish the template engine".to_string(), true),
                ("Incorporate feedback".to_string(), false),
            ]),
        }));
    let listener = TcpListener::bind("0.0.0.0:46811")
        .await
        .expect("failed to bind");

    println!("Todo app started; Visit http://localhost:46811 in your browser to see it!");

    axum::serve(listener, app).await.expect("failed to serve");
}

async fn index(State(state): State<Arc<AppState>>) -> Html<String> {
    Html(
        state
            .templates
            .render(
                "index",
                value! {
                    items: state
                        .items
                        .read()
                        .await
                        .iter()
                        .enumerate()
                        .map(|(idx, (item, complete))| value!(
                                *complete,
                                item.to_string(),
                                i64::try_from(idx).expect("too many items")
                        ))
                        .collect::<Vec<_>>()
                },
            )
            .expect("failed rendering index template"),
    )
}

#[derive(Deserialize)]
struct UpdateData {
    id: usize,
    done: Option<String>,
}

async fn update(
    State(state): State<Arc<AppState>>,
    Form(UpdateData { id, done }): Form<UpdateData>,
) -> Redirect {
    let mut items = state.items.write().await;

    items
        .get_mut(id)
        .expect("requested to update non existent item")
        .1 = done.is_some();

    Redirect::to("/")
}

#[derive(Deserialize)]
struct DeleteData {
    id: usize,
}

async fn delete(
    State(state): State<Arc<AppState>>,
    Form(DeleteData { id }): Form<DeleteData>,
) -> Redirect {
    let mut items = state.items.write().await;

    assert!(id < items.len(), "requested to delete non existent item");

    items.remove(id);

    Redirect::to("/")
}

#[derive(Deserialize)]
struct AddData {
    item: String,
}

async fn add(
    State(state): State<Arc<AppState>>,
    Form(AddData { item }): Form<AddData>,
) -> Redirect {
    let mut items = state.items.write().await;

    items.push((item, false));

    Redirect::to("/")
}
