use uuid::Uuid;
use diesel::prelude::*;
use diesel::result::Error as DieselError;
use diesel_derive_enum::DbEnum;
use rocket::request::{FromRequest, Request, Outcome};
use rocket::outcome::try_outcome;
use serde::{Deserialize};
use argon2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString};
use argon2::password_hash::errors::Error as PasswordHashError;
use rand::rngs::OsRng;
use argon2::{Algorithm, Argon2, Version, Params};


use crate::schema::*;
use crate::DbConn;

use crate::models::errors::{UserError, ErrorResponse, make_500};
use crate::models::zone::Zone;
use crate::models::session::Session;


#[derive(Debug, DbEnum, Deserialize, Clone)]
#[serde(rename_all = "lowercase")]
pub enum Role {
    #[db_rename = "admin"]
    Admin,
    #[db_rename = "zoneadmin"]
    ZoneAdmin,
}

// TODO: Store Uuid instead of string??
#[derive(Debug, Queryable, Identifiable, Insertable)]
#[table_name = "user"]
pub struct User {
    pub id: String,
}

#[derive(Debug, Queryable, Identifiable, Insertable)]
#[table_name = "localuser"]
#[primary_key(user_id)]
pub struct LocalUser {
    pub user_id: String,
    pub username: String,
    pub password: String,
    pub role: Role,
}

#[derive(Debug, Queryable, Identifiable, Insertable)]
#[table_name = "user_zone"]
#[primary_key(user_id, zone_id)]
pub struct UserZone {
    pub user_id: String,
    pub zone_id: String,
}

#[derive(Debug, Deserialize)]
pub struct CreateUserRequest {
    pub username: String,
    pub password: String,
    pub email: String,
    pub role: Option<Role>
}

#[derive(Debug)]
pub struct UserInfo {
    pub id: String,
    pub role: Role,
    pub username: String,
}

impl UserInfo {
    pub fn is_admin(&self) -> bool {
        matches!(self.role, Role::Admin)
    }

    pub fn check_admin(&self) -> Result<(), UserError> {
        if self.is_admin() {
            Ok(())
        } else {
            Err(UserError::PermissionDenied)
        }
    }

    pub fn get_zone(&self, conn: &diesel::SqliteConnection, zone_name: &str) -> Result<Zone, UserError> {
        use crate::schema::user_zone::dsl::*;
        use crate::schema::zone::dsl::*;

        let (res_zone, _): (Zone, UserZone) = zone.inner_join(user_zone)
            .filter(name.eq(zone_name))
            .filter(user_id.eq(&self.id))
            .get_result(conn)
            .map_err(|e| match e {
                DieselError::NotFound => UserError::ZoneNotFound,
                other => UserError::DbError(other)
            })?;

        Ok(res_zone)
    }

    pub fn get_zones(&self, conn: &diesel::SqliteConnection) -> Result<Vec<Zone>, UserError> {
        use crate::schema::user_zone::dsl::*;
        use crate::schema::zone::dsl::*;

        let res: Vec<(Zone, UserZone)> = zone.inner_join(user_zone)
            .filter(user_id.eq(&self.id))
            .get_results(conn)
            .map_err(UserError::DbError)?;

        Ok(res.into_iter().map(|(z, _)| z).collect())
    }
}

#[rocket::async_trait]
impl<'r> FromRequest<'r> for UserInfo {
    type Error = ErrorResponse;

    async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
        let session = try_outcome!(request.guard::<Session>().await.map_failure(make_500));
        let conn = try_outcome!(request.guard::<DbConn>().await.map_failure(make_500));

        conn.run(move |c| {
            match LocalUser::get_user_by_uuid(c, &session.user_id) {
                Err(e) => ErrorResponse::from(e).into(),
                Ok(d) => Outcome::Success(d),
            }
        }).await
    }
}

impl LocalUser {
    pub fn hash_password(password: &str) -> String {
        let salt = SaltString::generate(&mut OsRng);

        // https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
        let argon2 = Argon2::new(
            Algorithm::Argon2id,
            Version::V0x13, // v19
            Params::new(15000, 2, 1, None).expect("password param error"),
        );
        argon2.hash_password(password.as_bytes(), &salt).expect("password hash failed").to_string()
    }

    pub fn verify_password(password: &str, password_hash: &str) -> Result<bool, PasswordHashError> {
        let parsed_hash = PasswordHash::new(&password_hash)?;

        Ok(Argon2::default().verify_password(password.as_bytes(), &parsed_hash).is_ok())
    }


    pub fn create_user(conn: &diesel::SqliteConnection, user_request: CreateUserRequest) -> Result<UserInfo, UserError> {
        use crate::schema::localuser::dsl::*;
        use crate::schema::user::dsl::*;

        let new_user_id = Uuid::new_v4().to_simple().to_string();

        let new_user = User {
            id: new_user_id.clone(),
        };

        let new_localuser = LocalUser {
            user_id: new_user_id,
            username: user_request.username.clone(),
            password: LocalUser::hash_password(&user_request.password),
            role: if let Some(user_role) = user_request.role { user_role } else { Role::ZoneAdmin },
        };

        let res = UserInfo {
            id: new_user.id.clone(),
            role: new_localuser.role.clone(),
            username: new_localuser.username.clone(),
        };

        conn.immediate_transaction(|| -> diesel::QueryResult<()> {
            diesel::insert_into(user)
                .values(new_user)
                .execute(conn)?;

            diesel::insert_into(localuser)
                .values(new_localuser)
                .execute(conn)?;

            Ok(())
        }).map_err(|e| match e {
            DieselError::DatabaseError(diesel::result::DatabaseErrorKind::UniqueViolation, _) => UserError::UserConflict,
            other => UserError::DbError(other)
        })?;

        Ok(res)
    }

    pub fn get_user_by_creds(
        conn: &diesel::SqliteConnection,
        request_username: &str,
        request_password: &str
    ) ->  Result<UserInfo, UserError> {

        use crate::schema::localuser::dsl::*;
        use crate::schema::user::dsl::*;

        let (client_user, client_localuser): (User, LocalUser) = user.inner_join(localuser)
            .filter(username.eq(request_username))
            .get_result(conn)
            .map_err(|e| match e {
                DieselError::NotFound => UserError::BadCreds,
                other => UserError::DbError(other)
            })?;

        if !LocalUser::verify_password(&request_password, &client_localuser.password)? {
            return Err(UserError::BadCreds);
        }

        Ok(UserInfo {
            id: client_user.id,
            role: client_localuser.role,
            username: client_localuser.username,
        })
    }

    pub fn get_user_by_uuid(conn: &diesel::SqliteConnection, request_user_id: &str) -> Result<UserInfo, UserError> {
        use crate::schema::localuser::dsl::*;
        use crate::schema::user::dsl::*;

        let (client_user, client_localuser): (User, LocalUser) = user.inner_join(localuser)
            .filter(id.eq(request_user_id))
            .get_result(conn)
            .map_err(|e| match e {
                DieselError::NotFound => UserError::NotFound,
                other => UserError::DbError(other)
            })?;

        Ok(UserInfo {
            id: client_user.id,
            role: client_localuser.role,
            username: client_localuser.username,
        })
    }
}