use serde_json::Value;
use serde::Serialize;

use mysql_async;
use mysql_async::{Conn, Pool};
use mysql_async::error::Error;
use mysql_async::prelude::{params, Queryable};

use hyper::{Response, Body, StatusCode};

use chrono::Utc;

use crate::db_types::{UBigInt, BigInt};
use crate::members::{self, Member};

#[derive(Serialize)]
struct Invite {
    id: BigInt,
    uses: Option<BigInt>, // optional because some links are permanent
    expires: bool,
}
/*
 * Error handling: 
 * All errors raisable from this module come from mysql_async and thus
 * are of the enum mysql_async::error::Error
*/

async fn valid_invite(pool: &Pool, id: BigInt) -> Result<bool, Error>{
    /*
     * Fetches an invite from the database to check for validity
     */
    let conn = pool.get_conn().await?;
    let db_fetch_result: (Conn, Option<(Option<BigInt>, bool)>)  =
        conn.first_exec("SELECT uses, expires FROM invites WHERE id = :id", 
        params!{"id" => id}).await?;

    if let Some(row) = db_fetch_result.1 {
        // if expires at all
        if row.1 {
            let now = Utc::now().timestamp();
            // old?
            let mut status = now > id;
            // used?
            if row.0.is_some() && status == false {
                status = row.0.unwrap() <= 0; // safe unwrap since we know its Some(_)
            }
            return Ok(status)
        }
        // no expiry date? no problem
        return Ok(true);
    }
    // prolly not a real id
    else {
        return Ok(false);
    }

}

async fn use_invite(pool: &Pool, code: Option<BigInt>) -> Option<Member>{
    /*
     * Attempts to change the state of the current invite being provided
     */
    use crate::perms::GENERAL_NEW;
    let id = match code {
        Some(id) => id,
        None => 0
    };

    if let Ok(valid) = valid_invite(pool, id).await {
        if valid {
            match members::insert_new_member(pool, "Anonymous".into(), GENERAL_NEW).await {
                Ok(member) => return Some(member),
                Err(_) =>  return None
            }
        }
        else {
            return None;
        }
    }
    else {
        return None;
    }
}

pub async fn join(pool: &Pool, response: &mut Response<Body>, params: Value) {
    /*
     * Main dispatcher for dealing with an attempted join via a given invide code
     */
    let code = match params.get("invite-id") {
        Some(val) => val.as_i64(),
        None => None
    };

    match use_invite(&pool, code).await {
        Some(new_account) => *response.body_mut() = Body::from(serde_json::to_string(&new_account).unwrap()),
        None => {
        }
    }
}

async fn insert_new_invite(pool: &Pool, invite: &Invite) -> Result<(), Error>{
    let conn = pool.get_conn().await?;
    conn.prep_exec(
        "INSERT INTO invites (id, uses, expires)
        VALUES (:id, :uses, :expires)", params!{
            "id"    => invite.id,
            "uses"  => invite.uses,
            "expires" => invite.expires
    }).await?;

    Ok(())
}

async fn process_expires_parameter(p: &Pool, exp: &Value, id: UBigInt) -> bool {
    // TODO: fix this somewhat unsafe code
    // NOTE: its unsafe because of these lazy as heck unwraps everywhere
    use crate::perms::{CREATE_PERM_INVITES, CREATE_TMP_INVITES};
    let conn = p.get_conn().await.unwrap();
    let db_tup: (Conn, Option<UBigInt>) = conn.first_exec(
            "SELECT permissions FROM members WHERE id = :id", 
            params!{"id" => id})
            .await.unwrap();
    // depending on what type of invite we requested we should make sure we have the 
    // right permissions to do so
    let real_perms = db_tup.1.unwrap(); // safe via auth module
    if let Some(exp) = exp.as_bool() {
        // perma?
        if exp {
            return (real_perms & CREATE_PERM_INVITES) == CREATE_PERM_INVITES;
        }
        else {
            return (real_perms & CREATE_TMP_INVITES) == CREATE_TMP_INVITES;
        }
    }
    else {
        return false;
    }
}

pub async fn create(pool: &Pool, response: &mut Response<Body>, params: Value) {
    /*
     * Creates a new invite
     */

    let id = params.get("id").unwrap().as_u64().unwrap();

    let use_count = match params.get("uses")  {
        Some(val) => val.as_i64(),
        None => None
    };

    let expires = match params.get("expires") {
        Some(exp_val) => process_expires_parameter(pool, exp_val, id).await,
        None => true
    };

    let invite = Invite {
        id: (Utc::now() + chrono::Duration::minutes(30)).timestamp(),
        uses: use_count,
        expires: expires
    };

    match insert_new_invite(&pool, &invite).await {
        Ok(_) => {},
        Err(mysqle) => {
            println!("\tINVITES::CREATE::ERROR: {}", mysqle);
            *response.status_mut() = StatusCode::BAD_REQUEST;
        }
    }
}


#[cfg(test)]
mod invites_test {
    /*
     * INVITE CREATION 
     * Good - Bad - Malicious
     */

    use crate::testing::{get_pool, hyper_resp};
    use hyper::StatusCode;
    use serde_json::Value;

    #[tokio::test]
    async fn create_invite_good() {
        // Generation of data
        let p = get_pool();
        let mut resp = hyper_resp();
        // expected params 
        let params: Value = serde_json::from_str(r#"
            {
                "uses": 3,
                "expire": null
            }
        "#).unwrap();

        // Collection
        super::join(&p, &mut resp, params).await;
        let _ = p.disconnect().await;

        assert_eq!(StatusCode::OK, resp.status());
    }
}
