use crate::constants::{PASSWORD_RESET_TOKEN_LENGTH, PASSWORD_RESET_TOKEN_LIFETIME};
use crate::constants::database_tables_names::{USER_ACCESS_TOKENS_TABLE, USERS_TABLE};
use crate::data::account_export::AccountExport;
use crate::data::api_client::APIClient;
use crate::data::error::{ExecError, ResultBoxError};
use crate::data::general_settings::GeneralSettings;
use crate::data::lang_settings::LangSettings;
use crate::data::new_account::NewAccount;
use crate::data::security_settings::SecuritySettings;
use crate::data::user::{AccountImageVisibility, UserID, UserPageStatus};
use crate::data::user_token::UserAccessToken;
use crate::helpers::{comments_helper, conversations_helper, database, friends_helper, groups_helper, likes_helper, movies_helper, notifications_helper, posts_helper, survey_helper, user_helper};
use crate::helpers::database::{DeleteQuery, InsertQuery, QueryInfo};
use crate::utils::crypt_utils::{crypt_pass, rand_str};
use crate::utils::date_utils::{mysql_date, time};
use crate::utils::user_data_utils::user_data_path;

/// Account helper
///
/// @author Pierre Hubert

/// Create a new account
pub fn create(new_account: &NewAccount) -> ResultBoxError {
    database::InsertQuery::new(USERS_TABLE)
        .add_str("nom", &new_account.first_name)
        .add_str("prenom", &new_account.last_name)
        .add_str("date_creation", &mysql_date())
        .add_str("mail", &new_account.email)
        .add_str("password", &crypt_pass(&new_account.password)?)
        .insert_drop_result()
}

/// Attempt to sign-in user
///
/// In this version of the api, we consider that there is only one login token required
/// This is why I returns just a simple string, the token created for the user in case of success
pub fn login_user(email: &str, password: &str, client: &APIClient) -> ResultBoxError<String> {
    let user = user_helper::find_user_by_email(email)?;

    // Validate user password
    let password = crypt_pass(password)?;
    if !user.password.eq(&password) {
        return Err(ExecError::boxed_new("The user gave an invalid password!"));
    }

    // Check if we already have a login token for this user
    if let Ok(token) = get_client_tokens(&user.id, client) {
        return Ok(token.token);
    }


    // Create new login tokens
    let new_token = UserAccessToken {
        user_id: user.id.clone(),
        client_id: client.id,
        token: rand_str(150),
    };

    // Save it
    database::insert(
        InsertQuery::new(USER_ACCESS_TOKENS_TABLE)
            .add_user_id("user_id", &new_token.user_id)
            .add_u32("service_id", client.id)
            .add_str("token1", &new_token.token)
            .add_str("token2", "dummy_data")
    )?;

    Ok(new_token.token)
}

/// Get user login tokens
fn get_client_tokens(user_id: &UserID, client: &APIClient) -> ResultBoxError<UserAccessToken> {
    database::query_row(
        QueryInfo::new(USER_ACCESS_TOKENS_TABLE)
            .cond_user_id("user_id", user_id)
            .cond_u32("service_id", client.id),
        |res| {
            Ok(UserAccessToken {
                user_id: res.get_user_id("user_id")?,
                client_id: res.get_int64("service_id")? as u32,
                token: res.get_str("token1")?,
            })
        },
    )
}

/// Find a user ID based on login token
pub fn get_user_by_login_token(token: &str, client: &APIClient) -> ResultBoxError<UserID> {
    database::query_row(
        QueryInfo::new(USER_ACCESS_TOKENS_TABLE)
            .cond_u32("service_id", client.id)
            .cond("token1", token)
            .add_field("user_id"),
        |res| res.get_user_id("user_id"),
    )
}

/// Check out whether an email address exists or not
pub fn exists_mail(mail: &str) -> ResultBoxError<bool> {
    database::QueryInfo::new(USERS_TABLE)
        .cond("mail", mail)
        .exec_count()
        .map(|r| r > 0)
}

/// Destroy a given user login tokens
pub fn destroy_login_tokens(id: &UserID, client: &APIClient) -> ResultBoxError<()> {
    database::delete(DeleteQuery::new(USER_ACCESS_TOKENS_TABLE)
        .cond_u32("service_id", client.id)
        .cond_user_id("user_id", id)
    )?;

    Ok(())
}

/// Destroy all login tokens of a user
pub fn destroy_all_user_tokens(id: &UserID) -> ResultBoxError {
    database::DeleteQuery::new(USER_ACCESS_TOKENS_TABLE)
        .cond_user_id("user_id", id)
        .exec()
}

/// Generate a new password reset token
pub fn generate_password_reset_token(user_id: &UserID) -> ResultBoxError<String> {
    let token = rand_str(PASSWORD_RESET_TOKEN_LENGTH);

    database::UpdateInfo::new(USERS_TABLE)
        .cond_user_id("ID", user_id)
        .set_str("password_reset_token", &token)
        .set_u64("password_reset_token_time_create", time())
        .exec()?;

    Ok(token)
}

/// Remove password reset token for a given user
pub fn destroy_password_reset_token_for_user(user_id: &UserID) -> ResultBoxError {
    database::UpdateInfo::new(USERS_TABLE)
        .cond_user_id("ID", user_id)
        .set_str("password_reset_token", "")
        .set_u64("password_reset_token_time_create", 0)
        .exec()
}

/// Get the ID of a user based on a password reset token
pub fn get_user_id_from_password_reset_token(token: &str) -> ResultBoxError<UserID> {
    database::QueryInfo::new(USERS_TABLE)
        .cond("password_reset_token", token)
        .set_custom_where("password_reset_token_time_create > ?")
        .add_custom_where_argument_u64(time() - PASSWORD_RESET_TOKEN_LIFETIME)
        .query_row(|r| r.get_user_id("ID"))
}

/// Check current user's password
pub fn check_user_password(user_id: &UserID, password: &str) -> ResultBoxError<bool> {
    let crypt_pass = crypt_pass(password)?;

    database::QueryInfo::new(USERS_TABLE)
        .cond_user_id("ID", user_id)
        .cond("password", &crypt_pass)
        .exec_count()
        .map(|r| r > 0)
}

/// Change the password of a user
pub fn change_password(user_id: &UserID, new_password: &String) -> ResultBoxError {
    database::UpdateInfo::new(USERS_TABLE)
        .cond_user_id("ID", user_id)
        .set_str("password", &crypt_pass(new_password)?)
        .exec()
}

/// Check out whether a virtual directory is taken by a user or not
pub fn check_user_directory_availability(dir: &str, user_id: Option<UserID>) -> ResultBoxError<bool> {
    let found_user = user_helper::find_user_by_virtual_directory(dir);

    match (found_user, user_id) {

        // A user was found, but we did not specify a user
        (Ok(_), None) => Ok(false),

        // A user was found, and we specified a user ID, we check if the IDs are the same
        (Ok(user), Some(id)) => Ok(user.id == id),

        // No user was found, virtual directory is considered as available
        (Err(_), _) => Ok(true)
    }
}

/// Update the last activity of a user
pub fn update_last_activity(user_id: &UserID) -> ResultBoxError {
    database::UpdateInfo::new(USERS_TABLE)
        .cond_user_id("ID", user_id)
        .set_u64("last_activity", time())
        .exec()
}


/// Set new general settings of an account
pub fn set_general(settings: &GeneralSettings) -> ResultBoxError {
    database::UpdateInfo::new(USERS_TABLE)
        .cond_user_id("ID", &settings.id)
        .set_str("prenom", &settings.first_name)
        .set_str("nom", &settings.last_name)
        .set_legacy_bool("public", settings.page_status != UserPageStatus::PRIVATE)
        .set_legacy_bool("pageouverte", settings.page_status == UserPageStatus::OPEN)
        .set_legacy_bool("bloquecommentaire", settings.block_comments)
        .set_legacy_bool("autoriser_post_amis", settings.allow_posts_from_friends)
        .set_legacy_bool("autorise_mail", settings.allow_mails)
        .set_legacy_bool("liste_amis_publique", settings.friends_list_public)
        .set_opt_str("sous_repertoire", settings.virtual_directory.clone())
        .set_opt_str("site_web", settings.personal_website.clone())
        .set_opt_str("public_note", settings.public_note.clone())
        .exec()
}

/// Set new language settings
pub fn set_language_settings(settings: &LangSettings) -> ResultBoxError {
    database::UpdateInfo::new(USERS_TABLE)
        .cond_user_id("ID", &settings.id)
        .set_str("lang", &settings.lang)
        .exec()
}

/// Set new security settings
pub fn set_security_settings(settings: &SecuritySettings) -> ResultBoxError {
    database::UpdateInfo::new(USERS_TABLE)
        .cond_user_id("ID", &settings.id)
        .set_opt_str("question1", settings.question1.question())
        .set_opt_str("reponse1", settings.question1.answer())
        .set_opt_str("question2", settings.question2.question())
        .set_opt_str("reponse2", settings.question2.answer())
        .exec()
}

/// Delete user account image
pub fn delete_account_image(user_id: &UserID) -> ResultBoxError {
    let user = user_helper::find_user_by_id(user_id)?;

    if !user.has_account_image() {
        return Ok(());
    }

    let path = user_data_path(user.account_image_path.unwrap().as_ref());

    if path.exists()
    {
        std::fs::remove_file(path)?;
    }

    database::UpdateInfo::new(USERS_TABLE)
        .cond_user_id("ID", user_id)
        .set_str("account_image_path", "")
        .exec()
}

/// Set a new account image
pub fn set_account_image(user_id: &UserID, uri: &String) -> ResultBoxError {
    // First, delete the previous account image
    delete_account_image(user_id)?;

    // Update database
    database::UpdateInfo::new(USERS_TABLE)
        .cond_user_id("ID", user_id)
        .set_str("account_image_path", uri)
        .exec()
}

/// Set account image visibility level
pub fn set_account_image_visibility(user_id: &UserID, level: AccountImageVisibility) -> ResultBoxError {
    database::UpdateInfo::new(USERS_TABLE)
        .cond_user_id("ID", user_id)
        .set_str("account_image_visibility", &level.to_db())
        .exec()
}

/// Export an account's data
pub fn export(user_id: &UserID) -> ResultBoxError<AccountExport> {
    let mut data = AccountExport {
        user: user_helper::find_user_by_id(user_id)?,
        posts: posts_helper::export_all_posts_user(user_id)?,
        comments: comments_helper::export_all_user(user_id)?,
        likes: likes_helper::export_all_user(user_id)?,
        survey_responses: survey_helper::export_all_user_responses(user_id)?,
        movies: movies_helper::get_list_user(user_id)?,
        all_conversation_messages: conversations_helper::export_all_user_messages(user_id)?,
        conversations: conversations_helper::get_list_user(user_id)?,
        conversation_messages: Default::default(),
        friends_list: friends_helper::GetFriendsQuery::new(user_id).exec()?,
        groups: groups_helper::get_list_user(user_id, false)?,
    };

    // Process conversation messages
    for conv in &data.conversations {
        data.conversation_messages
            .insert(conv.id, conversations_helper::get_all_messages(conv.id)?);
    }

    Ok(data)
}

/// Delete a user's account
pub fn delete(user_id: &UserID) -> ResultBoxError {
    // TODO : close all websockets of user

    // Delete all group membership
    groups_helper::delete_all_user_groups(user_id)?;

    // Delete all user comments
    comments_helper::delete_all_user(user_id)?;

    // Delete all user posts
    posts_helper::delete_all_user(user_id)?;

    // Delete all responses of user to surveys
    survey_helper::delete_all_user_responses(user_id)?;

    // Delete all the likes created by the user
    likes_helper::delete_all_user(user_id)?;

    // Delete all user movies
    movies_helper::delete_all_user(user_id)?;

    // Delete all conversation messages
    conversations_helper::delete_all_user_messages(user_id)?;

    // Remove the user from all its conversations
    conversations_helper::delete_all_user_conversations(user_id)?;

    // Delete all the notifications related with the user
    notifications_helper::delete_all_related_with_user(user_id)?;

    // Delete all user friends, including friendship requests
    friends_helper::delete_all_user(user_id)?;

    // TODO : continue work here

    Ok(())
}