use std::error::Error;
use log::info;
use sqlx::PgPool;
use uuid::Uuid;
use crate::config::TFConfig;
use std::time::SystemTime;
use std::time::UNIX_EPOCH;
use totp_rs::{Secret, TOTP};
use crate::util::{TOTP_ALGORITHM, TOTP_DIGITS, TOTP_ISSUER, TOTP_SKEW, TOTP_STEP};

// https://admin.defined.net/auth/magic-link?email=coredoescode%40gmail.com&token=ml-ckBsgw_5IdK5VYgseBYcoV_v_cQjtdq1re_RhDu_MKg
pub async fn send_magic_link(id: i64, email: String, db: &PgPool, config: &TFConfig) -> Result<(), Box<dyn Error>> {
    let otp = format!("ml-{}", Uuid::new_v4());
    let otp_url = config.base.join(&format!("/auth/magic-link?email={}&token={}", urlencoding::encode(&email.clone()), otp.clone())).unwrap();
    sqlx::query!("INSERT INTO magic_links (id, user_id, expires_on) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING;", otp, id as i32, SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i32 + config.magic_links_valid_for as i32).execute(db).await?;
    // TODO: send email
    info!("sent magic link {} to {}, valid for {} seconds", otp_url, email.clone(), config.magic_links_valid_for);
    Ok(())
}

pub async fn generate_session_token(user_id: i64, db: &PgPool, config: &TFConfig) -> Result<String, Box<dyn Error>> {
    let token = format!("st-{}", Uuid::new_v4());
    sqlx::query!("INSERT INTO session_tokens (id, user_id, expires_on) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING;", token, user_id as i32, SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i32 + config.session_tokens_valid_for as i32).execute(db).await?;
    Ok(token)
}
pub async fn validate_session_token(token: String, db: &PgPool) -> Result<i64, Box<dyn Error>> {
    Ok(sqlx::query!("SELECT user_id FROM session_tokens WHERE id = $1 AND expires_on > $2", token, SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i32).fetch_one(db).await?.user_id as i64)
}

pub async fn generate_auth_token(user_id: i64, session_id: String, db: &PgPool) -> Result<String, Box<dyn Error>> {
    let token = format!("at-{}", Uuid::new_v4());
    sqlx::query!("INSERT INTO auth_tokens (id, session_token, user_id) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING;", token, session_id, user_id as i32).execute(db).await?;
    Ok(token)
}
pub async fn validate_auth_token(token: String, session_id: String, db: &PgPool) -> Result<(), Box<dyn Error>> {
    validate_session_token(session_id.clone(), db).await?;
    sqlx::query!("SELECT * FROM auth_tokens WHERE id = $1 AND session_token = $2", token, session_id).fetch_one(db).await?;
    Ok(())
}


/*
CREATE TABLE totp_create_tokens (
    id VARCHAR(39) NOT NULL PRIMARY KEY,
    expires_on INTEGER NOT NULL,
    totp_otpurl VARCHAR(3000) NOT NULL,
    totp_secret VARCHAR(128) NOT NULL
);
 */

pub async fn create_totp_token(email: String, db: &PgPool, config: &TFConfig) -> Result<(String, TOTP), Box<dyn Error>> {
    // create the TOTP parameters

    let secret = Secret::generate_secret();
    let totpmachine = TOTP::new(TOTP_ALGORITHM, TOTP_DIGITS, TOTP_SKEW, TOTP_STEP, secret.to_bytes().unwrap(), Some(TOTP_ISSUER.to_string()), email).unwrap();
    let otpurl = totpmachine.get_url();
    let otpsecret = totpmachine.get_secret_base32();

    let otpid = format!("totp-{}", Uuid::new_v4());

    sqlx::query!("INSERT INTO totp_create_tokens (id, expires_on, totp_otpurl, totp_secret) VALUES ($1, $2, $3, $4);", otpid.clone(), (SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64 + config.totp_verification_valid_for) as i32, otpurl, otpsecret).execute(db).await?;

    Ok((otpid, totpmachine))
}

pub async fn verify_totp_token(otpid: String, email: String, db: &PgPool) -> Result<TOTP, Box<dyn Error>> {
    let totprow = sqlx::query!("SELECT * FROM totp_create_tokens WHERE id = $1 AND expires_on > $2", otpid, SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i32).fetch_one(db).await?;
    let secret = Secret::Encoded(totprow.totp_secret);
    let totpmachine = TOTP::new(TOTP_ALGORITHM, TOTP_DIGITS, TOTP_SKEW, TOTP_STEP, secret.to_bytes().unwrap(), Some(TOTP_ISSUER.to_string()), email).unwrap();

    if totpmachine.get_url() != totprow.totp_otpurl {
        return Err("OTPURLs do not match (email does not match?)".into())
    }

    Ok(totpmachine)
}

pub async fn use_totp_token(otpid: String, email: String, db: &PgPool) -> Result<TOTP, Box<dyn Error>> {
    let totpmachine = verify_totp_token(otpid.clone(), email.clone(), db).await?;
    sqlx::query!("DELETE FROM totp_create_tokens WHERE id = $1", otpid).execute(db).await?;
    sqlx::query!("UPDATE users SET totp_otpurl = $1, totp_secret = $2, totp_verified = 1 WHERE email = $3", totpmachine.get_url(), totpmachine.get_secret_base32(), email).execute(db).await?;
    Ok(totpmachine)
}

pub async fn get_totpmachine(user: i32, db: &PgPool) -> Result<TOTP, Box<dyn Error>> {
    let user = sqlx::query!("SELECT totp_secret, totp_otpurl, email FROM users WHERE id = $1", user).fetch_one(db).await?;
    let secret = Secret::Encoded(user.totp_secret);
    Ok(TOTP::new(TOTP_ALGORITHM, TOTP_DIGITS, TOTP_SKEW, TOTP_STEP, secret.to_bytes().unwrap(), Some(TOTP_ISSUER.to_string()), user.email).unwrap())
}

pub async fn user_has_totp(user: i32, db: &PgPool) -> Result<bool, Box<dyn Error>> {
    Ok(sqlx::query!("SELECT totp_verified FROM users WHERE id = $1", user).fetch_one(db).await?.totp_verified == 1)
}