use rocket::{
    catch, catchers,
    fairing::AdHoc,
    form::Form,
    fs::FileServer,
    get,
    http::Cookie,
    post,
    request::FlashMessage,
    response::{Flash, Redirect},
    routes,
    time::{Duration, OffsetDateTime},
    Build, FromForm, Request, Rocket, State,
};
use rocket_dyn_templates::Template;
use serde::Deserialize;
use sqlx::SqlitePool;
use tera::Context;

use crate::model::{
    notification::Notification,
    role::Role,
    user::{User, UserWithRoles},
};

pub(crate) mod admin;
mod auth;
pub(crate) mod board;
mod boatdamage;
mod boatreservation;
mod cox;
mod ergo;
mod log;
mod misc;
mod notification;
mod planned;
mod stat;

#[derive(FromForm, Debug)]
struct LoginForm<'r> {
    name: &'r str,
    password: &'r str,
}

#[get("/")]
async fn index(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_>>) -> Template {
    let mut context = Context::new();
    if let Some(msg) = flash {
        context.insert("flash", &msg.into_inner());
    }

    context.insert("notifications", &Notification::for_user(db, &user).await);
    context.insert("loggedin_user", &UserWithRoles::from_user(user, db).await);
    Template::render("index", context.into_json())
}

#[get("/steering")]
async fn steering(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_>>) -> Template {
    let mut context = Context::new();
    if let Some(msg) = flash {
        context.insert("flash", &msg.into_inner());
    }

    let bootskundige =
        User::all_with_role(db, &Role::find_by_name(db, "Bootsführer").await.unwrap()).await;

    let mut coxes = User::all_with_role(db, &Role::find_by_name(db, "cox").await.unwrap()).await;

    coxes.retain(|user| !bootskundige.contains(&user)); // Remove bootskundige from coxes list
    coxes.retain(|user| user.name != String::from("Externe Steuerperson"));

    context.insert("coxes", &coxes);
    context.insert("bootskundige", &bootskundige);

    context.insert("loggedin_user", &UserWithRoles::from_user(user, db).await);
    Template::render("steering", context.into_json())
}

#[post("/", data = "<login>")]
async fn wikiauth(db: &State<SqlitePool>, login: Form<LoginForm<'_>>) -> String {
    match User::login(db, login.name, login.password).await {
        Ok(_) => "SUCC".into(),
        Err(_) => "FAIL".into(),
    }
}

#[catch(401)] //Unauthorized
fn unauthorized_error(req: &Request) -> Redirect {
    // Save the URL the user tried to access, to be able to go there once logged in
    let mut redirect_cookie = Cookie::new("redirect_url", format!("{}", req.uri()));
    println!("{}", req.uri());
    redirect_cookie.set_expires(OffsetDateTime::now_utc() + Duration::hours(1));
    req.cookies().add_private(redirect_cookie);

    Redirect::to("/auth")
}

#[catch(403)] //forbidden
fn forbidden_error() -> Flash<Redirect> {
    Flash::error(Redirect::to("/"), "Keine Berechtigung für diese Aktion. Wenn du der Meinung bist, dass du das machen darfst, melde dich bitte bei it@rudernlinz.at.")
}

#[derive(Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct Config {
    rss_key: String,
    smtp_pw: String,
}

pub fn config(rocket: Rocket<Build>) -> Rocket<Build> {
    rocket
        .mount("/", routes![index, steering])
        .mount("/auth", auth::routes())
        .mount("/wikiauth", routes![wikiauth])
        .mount("/log", log::routes())
        .mount("/planned", planned::routes())
        .mount("/ergo", ergo::routes())
        .mount("/notification", notification::routes())
        .mount("/stat", stat::routes())
        .mount("/boatdamage", boatdamage::routes())
        .mount("/boatreservation", boatreservation::routes())
        .mount("/cox", cox::routes())
        .mount("/admin", admin::routes())
        .mount("/board", board::routes())
        .mount("/", misc::routes())
        .mount("/public", FileServer::from("static/"))
        .register("/", catchers![unauthorized_error, forbidden_error])
        .attach(Template::fairing())
        .attach(AdHoc::config::<Config>())
}

#[cfg(test)]
mod test {
    use rocket::{
        http::{ContentType, Status},
        local::asynchronous::Client,
    };
    use sqlx::SqlitePool;

    use crate::testdb;

    #[sqlx::test]
    fn test_index() {
        let db = testdb!();

        let rocket = rocket::build().manage(db.clone());
        let rocket = crate::tera::config(rocket);

        let client = Client::tracked(rocket).await.unwrap();
        let login = client
            .post("/auth")
            .header(ContentType::Form) // Set the content type to form
            .body("name=cox&password=cox"); // Add the form data to the request body;
        login.dispatch().await;

        let req = client.get("/");
        let response = req.dispatch().await;

        assert_eq!(response.status(), Status::Ok);

        assert!(response
            .into_string()
            .await
            .unwrap()
            .contains("Ruderassistent"));
    }

    #[sqlx::test]
    fn test_without_login() {
        let db = testdb!();

        let rocket = rocket::build().manage(db.clone());
        let rocket = crate::tera::config(rocket);

        let client = Client::tracked(rocket).await.unwrap();

        let req = client.get("/");
        let response = req.dispatch().await;

        assert_eq!(response.status(), Status::SeeOther);
        assert_eq!(response.headers().get("Location").next(), Some("/auth"));
    }

    #[sqlx::test]
    fn test_public() {
        let db = testdb!();

        let rocket = rocket::build().manage(db.clone());
        let rocket = crate::tera::config(rocket);

        let client = Client::tracked(rocket).await.unwrap();

        let req = client.get("/public/main.css");
        let response = req.dispatch().await;
        assert_eq!(response.status(), Status::Ok);

        let req = client.get("/public/main.js");
        let response = req.dispatch().await;
        assert_eq!(response.status(), Status::Ok);
    }
}
