use argon2::{
    Argon2, PasswordHash, PasswordHasher, PasswordVerifier,
    password_hash::{SaltString, rand_core::OsRng},
};
use rocket::{http::Status, serde::json::Json};
use rocket_db_pools::Connection;
use serde::{Deserialize, Serialize};

use crate::{auth::Session, db::Postgres, user::User};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PasswordForm {
    old_password: String,
    new_password: String,
}

#[post("/settings/password", data = "<form>")]
pub async fn change_password(
    session: Session,
    mut db: Connection<Postgres>,
    form: Json<PasswordForm>,
) -> Result<(), Status> {
    let mut user = User::get_by_id(session.user_id, &mut db)
        .await
        .ok_or(Status::NotFound)
        .inspect_err(|_| {
            tracing::error!(
                "Valid session does not have a valid user. ID: {}",
                session.user_id
            )
        })?;

    user.verify_password(&form.old_password)?;

    // old password is correct, so new one can be set.
    let salt = SaltString::generate(&mut OsRng);
    let hashed = Argon2::default()
        .hash_password(form.new_password.as_bytes(), &salt)
        .inspect_err(|e| tracing::error!("failed to hash password! {e}"))
        .map_err(|_| Status::InternalServerError)?
        .to_string();

    user.set_pass_hash(hashed, &mut db)
        .await
        .inspect_err(|e| tracing::error!("{e}"))
        .map_err(|_| Status::InternalServerError)?;

    Ok(())
}

#[derive(Deserialize, Debug, Clone)]
pub struct DisplayNameForm {
    pub display_name: Option<String>,
}

#[derive(Deserialize, Debug, Clone)]
pub struct PasswordAnd2fa {
    pub password: String,
    pub totp_code: Option<String>,
}

#[delete("/settings", data = "<data>")]
pub async fn delete_account(
    session: Session,
    mut db: Connection<Postgres>,
    data: Json<PasswordAnd2fa>,
) -> Result<(), Status> {
    let mut user = User::get_by_id(session.user_id, &mut db)
        .await
        .ok_or(Status::NotFound)
        .inspect_err(|_| {
            tracing::error!(
                "Valid session does not have a valid user. ID: {}",
                session.user_id
            )
        })?;

    user.verify_password(&data.password)?;

    if user.twofa_enabled {
        user.verify_2fa(data.totp_code.as_deref().unwrap_or(""))?;
    }

    user.delete(&mut db)
        .await
        .inspect_err(|e| tracing::error!("{e}"))
        .map_err(|_| Status::InternalServerError)?;

    Ok(())
}

#[patch("/settings/display_name", data = "<new>")]
pub async fn change_display_name(
    session: Session,
    mut db: Connection<Postgres>,
    new: Json<DisplayNameForm>,
) -> Result<(), Status> {
    let mut user = User::get_by_id(session.user_id, &mut db)
        .await
        .ok_or(Status::NotFound)
        .inspect_err(|_| {
            tracing::error!(
                "Valid session does not have a valid user. ID: {}",
                session.user_id
            )
        })?;

    user.set_display_name(new.display_name.clone(), &mut db)
        .await
        .inspect_err(|e| tracing::error!("{e}"))
        .map_err(|_| Status::InternalServerError)?;

    Ok(())
}

#[derive(Deserialize)]
pub struct UsernameForm {
    username: String,
}

#[patch("/settings/username", data = "<new>")]
pub async fn change_username(
    session: Session,
    mut db: Connection<Postgres>,
    new: Json<UsernameForm>,
) -> Result<(), Status> {
    let mut user = User::get_by_id(session.user_id, &mut db)
        .await
        .ok_or(Status::NotFound)
        .inspect_err(|_| {
            tracing::error!(
                "Valid session does not have a valid user. ID: {}",
                session.user_id
            )
        })?;

    user.set_username(new.username.clone(), &mut db)
        .await
        .inspect_err(|e| tracing::error!("{e}"))
        .map_err(|_| Status::InternalServerError)?;

    Ok(())
}
