use serde::{Serialize, Deserialize};
use bcrypt::{self, BcryptResult};
use mysql_async::Pool;
use chrono::{Utc, Duration};

use std::collections::HashMap;

use crate::routes;

use db::{member::Member, common::FromDB};
use db::Response;

use jsonwebtoken::EncodingKey;
lazy_static! {
    static ref HMAC_SECRET: Vec<u8> = {
        std::fs::read("hmac.secret").expect("Couldn't get HMAC secret")
    };

    static ref ENCODING_KEY: EncodingKey = {
        EncodingKey::from_secret(&HMAC_SECRET)
    };
}

#[derive(Debug, Serialize, Deserialize)]
struct Claim {
    sub: db::UBigInt, // user id
    exp: db::BigInt, // expiry date
    cookie: String,  // unique cookie value
}

impl Claim {
    pub fn new(id: db::UBigInt) -> Claim {
        Claim {
            sub: id,
            exp: Utc::now()
                .checked_add_signed(Duration::weeks(1))
                .expect("Couldn't generate an expirey date")
                .timestamp(),
            cookie: generate_cookie()
        }
    }
}


// used when we create a new users for the first time
#[derive(Debug)]
pub enum AuthReason {
    Good,     //passed regular check
    OpenAuth, // route does not require auth
    NoKey,    // key missing 
    BadKey,   // key is bad
    LoginValid, // used only to access the login route which is also our refresh
    ServerIssue(String) // for well 500's
}


fn valid_secret(given_pass: &str, hash: &str) -> bool {
    let result = bcrypt::verify(given_pass, hash);
    return match result {
        Ok(result) => result, 
        Err(e) => {
            eprintln!("{}", e);
            return false;
        }
    }
}

fn valid_perms(member: Member, path: &str) -> bool {
    use crate::perms;
    // if there are perms on the current path make sure the user has them
    if let Some(p) = perms::get_perm_mask(path) {
        return (p & member.permissions) == p;
    }
    // if no perms then we don't care
    else {
        return true;
    }
}

fn rng_secret(length: usize) -> String {
    use getrandom::getrandom;
    use base64::{encode_config, URL_SAFE};

    let mut buf: Vec<u8> = vec![0;length];
    getrandom(&mut buf).unwrap();
    encode_config(buf,URL_SAFE) 

}

pub fn generate_secret() -> String {
    /*
     * Generates a url-safe-plaintext secret for our db
     * */
     return rng_secret(64);
}

pub fn generate_cookie() -> String {
    return rng_secret(32) 
}

pub fn encrypt_secret(raw: &str) -> BcryptResult<String> {
    const BCRYPT_COST: u32 = 14;
    return bcrypt::hash(raw, BCRYPT_COST);
}


async fn valid_jwt(p: &Pool, token: &str)  -> AuthReason {
    use jsonwebtoken::{
        decode, DecodingKey,
        Validation, Algorithm
    };
    // crypto things that should prolly not fail assuming we're configured correctly
    let algo = Algorithm::HS512;
    let dk = DecodingKey::from_secret(&HMAC_SECRET);
    if let Ok(decoded) = decode::<Claim>(token, &dk, &Validation::new(algo)) {

        // subject used for querying speed NOT security
        let listed = db::auth::listed_jwt(p, decoded.claims.sub, token).await.unwrap();
        let active = Utc::now().timestamp() < decoded.claims.exp;

        return match listed && active {
            true => AuthReason::Good,
            false => AuthReason::BadKey
        };
    }
    else {
        return AuthReason::BadKey;
    }
}

fn login_params_from_qs<'value>(params: &HashMap<&str, &'value str>) 
        -> Option<(db::UBigInt, &'value str)> {

    return match (params.get("id"), params.get("secret")) {
        // partially accpept if both keys are present
        (Some(id), Some(secret)) => {
            let id_s: String = String::from(*id);

            match id_s.parse::<db::UBigInt>() {
                // full accpet if id can parse + secret is present
                Ok(id) => Some((id, secret)),
                _ => None
            }
        },

        _ => None
    }
}


pub async fn wall_entry<'path, 'pool, 'params>(
        path: &'path str, 
        pool: &'pool Pool, 
        params: &'params HashMap<&str, &str>) 
            -> AuthReason {

    // Dont need to auth if it's not required
    let open_path = routes::is_open(path);
    let jwt = params.get("jwt");

    if open_path { // ignore the parameters since they're irelevant
        return AuthReason::OpenAuth;
    }

    if let Some(jwt) = jwt {
        // get the headers here
        return valid_jwt(pool, jwt).await;
    }
    if let Some((id, secret)) = login_params_from_qs(params) {
        // Last chance we might be hitting the /login route so we have to do the  heavy auth flow

        if path != routes::AUTH_LOGIN {
            return AuthReason::BadKey;
        }
        else {
            return match Member::get(pool, id).await {
                Response::Row(user) => {
                    if valid_secret(secret, &user.secret) && valid_perms(user, path){
                        AuthReason::LoginValid
                    }
                    else {
                        AuthReason::BadKey
                    }
                },
                Response::Empty => AuthReason::BadKey,
                Response::Other(err)  => AuthReason::ServerIssue(err),
                _ => AuthReason::ServerIssue("db-lib returned garbage".into())
            }
        }
    }
    return AuthReason::NoKey;
}

pub async fn login_get_jwt(p: &Pool, response: &mut hyper::Response<hyper::Body>, params: HashMap<&str, &str>) {
    // Login data has already been validated at this point
    // Required data such as 'id' and 'secret' are there and validated
    use jsonwebtoken::{
        Header, Algorithm, 
        encode
    };
    use hyper::header::HeaderValue;
    use crate::http::{self, extract_uid};

    let id = extract_uid(&params); // only route where we have the "id is there guarantee"


    let claim = Claim::new(id);
    let header = Header::new(Algorithm::HS512);
    let encoded = encode(
        &header, 
        &claim, 
        &ENCODING_KEY).unwrap();
    
    match db::auth::add_jwt(p, id, &encoded).await {
        Ok(_) => {
            response.headers_mut().insert("Content-Type",
            HeaderValue::from_static("application/json"));

            http::set_json_body(response, serde_json::json!({"jwt": encoded}));
        },
        Err(e) => {
            eprintln!("{}", e);
            *response.status_mut() = hyper::StatusCode::INTERNAL_SERVER_ERROR;
        }
    };
}


#[cfg(test)]
mod auth_tests {
    use jsonwebtoken::{
        Header,  encode, Algorithm
    };
    #[test]
    fn validity_check() {
        use bcrypt::{hash, DEFAULT_COST};
        let plain = super::generate_secret();
        match hash(&plain, DEFAULT_COST) {
            Ok(hash) => assert_eq!(super::valid_secret(&plain, &hash), true),
            Err(err) => panic!("{}", err)
        }
    }

    #[test]
    fn verify_jwt() {
        let claim = super::Claim::new(123); // example claim that we send out
        let header = Header::new(Algorithm::HS512); // header that basically all clients get
        let encoded = encode(
            &header, 
            &claim, 
            &super::ENCODING_KEY).unwrap();

        use jsonwebtoken::{decode, DecodingKey, Validation};
        let dc = decode::<super::Claim>(
            &encoded, 
            &DecodingKey::from_secret(&super::HMAC_SECRET),
            &Validation::new(Algorithm::HS512)
        ); // decoding works yet fails on the debugger
        assert_eq!(dc.is_ok(), true);
        println!("{:?}", dc);
        println!("{}", encoded);

        let mut parts:Vec<String> =  Vec::new();
        for i in encoded.split('.') {
            parts.push(i.to_string());
        }
        let head = &parts[0];
        let claim = &parts[1];
        let sig = &parts[2];
        let res = jsonwebtoken::crypto::verify(
            sig, 
            &format!("{}.{}", head, claim), 
            &DecodingKey::from_secret(&super::HMAC_SECRET), 
            Algorithm::HS512);

        println!("{:?}", res);
    }
}
