use argon2::{
    Argon2, PasswordHash, PasswordHasher, PasswordVerifier,
    password_hash::{SaltString, rand_core::OsRng},
};
use jsonwebtoken::{EncodingKey, Header, encode};
use rocket::{
    http::{CookieJar, Status},
    response::{Redirect, status::BadRequest},
    serde::json::Json,
    time::OffsetDateTime,
};
use rocket_db_pools::Connection;
use rocket_dyn_templates::{Template, context};
use serde::{Deserialize, Serialize};
use uuid::Uuid;

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

#[derive(Serialize, Deserialize)]
pub struct SignupCredentials {
    pub email: String,
    pub username: String,
    pub password: String,
    pub access_token: String,
}

#[derive(Serialize, Deserialize)]
pub struct LoginCredentials {
    pub username: String,
    pub password: String,
}

#[derive(Serialize, Deserialize)]
pub struct AuthResponse {
    pub token: String,
    pub totp_required: bool,
}

#[get("/signup")]
pub async fn signup_page() -> Template {
    Template::render("signup", context!())
}

#[post("/signup", data = "<cred>")]
pub async fn signup(
    cred: Json<SignupCredentials>,
    jar: &CookieJar<'_>,
    mut db: Connection<Postgres>,
) -> Result<Json<AuthResponse>, Status> {
    let token_id = AccessToken::validate(&cred.access_token, &mut db)
        .await
        .map_err(|_| Status::Unauthorized)?;

    let salt = SaltString::generate(&mut OsRng);
    let hashed = Argon2::default()
        .hash_password(cred.password.as_bytes(), &salt)
        .map_err(|_| Status::InternalServerError)?
        .to_string();

    let result = sqlx::query!(
        "INSERT INTO users (email, username, pass_hash) VALUES ($1, $2, $3) RETURNING id",
        cred.email,
        cred.username,
        hashed,
    )
    .fetch_one(&mut **db)
    .await
    .map_err(|_| Status::InternalServerError)?;

    let jwt = Claims::new(result.id as usize, TokenScope::Full).encode();

    token_id
        .use_token(&mut db)
        .await
        .expect("unable to use access code");

    Ok(Json(AuthResponse {
        token: jwt,
        totp_required: false,
    }))
}

#[get("/login")]
pub async fn login_page() -> Template {
    Template::render("login", context!())
}

#[post("/login", data = "<cred>")]
pub async fn login(
    mut db: Connection<Postgres>,
    cred: Json<LoginCredentials>,
) -> Result<Json<AuthResponse>, Status> {
    println!("e");
    let row = sqlx::query!(
        "SELECT id, pass_hash, twofa_enabled FROM users WHERE username = $1",
        cred.username,
    )
    .fetch_one(&mut **db)
    .await
    .map_err(|_| Status::Unauthorized)?;

    println!("ok");

    // verify password as before
    let parsed_hash = PasswordHash::new(&row.pass_hash).map_err(|_| Status::InternalServerError)?;
    Argon2::default()
        .verify_password(cred.password.as_bytes(), &parsed_hash)
        .map_err(|_| Status::Unauthorized)?;

    println!("ok2");

    let user_id = row.id as usize;

    // issue either a partial or full token depending on 2FA status
    let (session, totp_required) = if row.twofa_enabled {
        (Claims::new(user_id, TokenScope::TotpPending), true)
    } else {
        (Claims::new(user_id, TokenScope::Full), false)
    };

    Ok(Json(AuthResponse {
        token: session.encode(),
        totp_required,
    }))
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AccessTokenForm {
    pub name: String,
    pub max_uses: usize,
    pub expiry_date: usize,
    pub start_date: usize,
}

#[get("/invite")]
pub async fn invite_page(_s: Session) -> Template {
    Template::render("invite", context! {})
}

#[post("/invite", data = "<form>")]
pub async fn generate_invite(
    session: Session,
    mut db: Connection<Postgres>,
    form: Json<AccessTokenForm>,
) -> Result<String, Status> {
    if form.start_date > form.expiry_date {
        return Err(Status::BadRequest);
    }

    let code = Uuid::new_v4().to_string();
    sqlx::query!(
        "INSERT INTO access_codes (name, code, creator_id, max_uses, created_at, expires_at)
        VALUES ($1, $2, $3, $4, $5, $6) RETURNING id",
        form.name,
        code,
        session.user_id as i32,
        form.max_uses as i32,
        OffsetDateTime::from_unix_timestamp_nanos(form.start_date as i128 * 1_000_000).unwrap(),
        OffsetDateTime::from_unix_timestamp_nanos(form.expiry_date as i128 * 1_000_000).unwrap()
    )
    .fetch_one(&mut **db)
    .await
    .map_err(|_| Status::InternalServerError)?;

    Ok(code)
}

pub struct AccessToken {
    id: i32,
    _code: String,
}

impl AccessToken {
    pub async fn validate(
        token: &str,
        db: &mut Connection<Postgres>,
    ) -> Result<AccessToken, String> {
        match sqlx::query!(
            "SELECT id FROM access_codes
                WHERE code = $1
                AND created_at < NOW()
                AND expires_at > NOW()
                AND uses < max_uses",
            token
        )
        .fetch_one(&mut ***db)
        .await
        {
            Ok(row) => Ok(AccessToken {
                id: row.id,
                _code: token.to_string(),
            }),
            Err(_) => Err(String::from("Invalid or Expired token!")),
        }
    }

    pub async fn use_token(&self, db: &mut Connection<Postgres>) -> Result<(), String> {
        sqlx::query!(
            "UPDATE access_codes SET uses = uses + 1 WHERE id = $1",
            self.id
        )
        .execute(&mut ***db)
        .await
        .map_err(|_| String::from("Invalid or Expired token!"))?;
        Ok(())
    }
}
