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

use async_trait::async_trait;

use crate::{UBigInt, BigInt};
use crate::common::FromDB;
use crate::{Response, no_conn};

#[allow(dead_code)]
pub struct Invite {
    pub id: BigInt,
    pub uses: Option<BigInt>,
    pub expires: bool
}

#[async_trait]
impl FromDB<Invite, bool> for Invite {
    type Row = Option<(BigInt, Option<BigInt>, bool)>;

    async fn get(p: &Pool, id: UBigInt) -> Response<Self> {
        // NOTE: cast is required for this as `id` is used as unix timestamp
        let id: BigInt = id as BigInt; 

        if id <= 0 {
            return Response::Empty;
        }

        if let Ok(conn) = p.get_conn().await {
            let q = "SELECT id, uses, expires FROM invites WHERE id = :id ";
            let result: Result<(Conn, Self::Row), SqlError> = 
                conn.first_exec(q, params!{"id" => id}).await;

            if let Ok((_, row)) = result {
                return match row {
                    Some(row) => Response::Row(Self {
                        id: id as BigInt,
                        uses: row.1,
                        expires: row.2
                    }),
                    None => Response::Empty
                }
            }
        }
        return Response::Empty;
    }

    async fn update(p: &Pool, row: Self) -> Response<Self> {
        let q = r#"UPDATE invites
        SET uses = :uses, expires: :exp
        WHERE id = :id
        "#;
        // forcibly udpate even if theres nothing there
        // this way we avoid doing an extra network hit
        if row.id <= 0 {
            return Response::Empty;
        }

        if let Ok(conn) = p.get_conn().await {
            let result: Result<Conn, SqlError> =
                conn.drop_exec(q, params!{
                    "id" => row.id,
                    "uses" => row.uses,
                    "exp" => row.expires
                }).await;

            return match result {
                Ok(_) => Response::Success,
                Err(_) => Response::Other(format!("Could not update entry {}", row.id))
            }
        }
        return Response::Empty;
    }
    async fn delete(p: &Pool, id: UBigInt) -> Response<Self> {
        if id <= 0 { // really lame "assertion" that each method has to use for invites since they all internally use 
            return Response::Empty;
        }
        if let Ok(conn) = p.get_conn().await {
            let q = "DELETE FROM invites WHERE id = :id";
            let result: Result<Conn, SqlError> =
                conn.drop_exec(q, params!{"id" => id as BigInt}).await;
            return match result {
                Ok(_) => Response::Success,
                Err(_) => Response::Other(format!("Could not delete {}", id))
            }
        }
        return Response::Success;
    }
    async fn filter(p: &Pool, expirey_flag: bool) -> Response<Self> {
        if let Ok(conn) = p.get_conn().await {
            let q = "SELECT id, uses, expires FROM invites WHERE expires = :exp";
            if let Ok(query) = conn.prep_exec(q, params!{"exp" => expirey_flag}).await {
                let mapping_r = query.map_and_drop(|row| {
                    let (id, uses): (BigInt, Option<BigInt>) = mysql_async::from_row(row);
                    Invite {
                        id: id,
                        uses: uses,
                        expires: expirey_flag
                    }
                }).await;
                return match mapping_r {
                    Ok((_, invites)) => Response::Set(invites),
                    Err(_) => Response::Empty
                }
            }
            else {
                return Response::Other(no_conn!("db::Invite::filter"));
            }
        }
        else {
            return Response::Other(no_conn!("db::Invites::filter"));
        }
    }
}

impl Invite {
    pub fn new(uses: Option<i64>, expires: bool) -> Invite {
        use chrono::Utc;

        Invite {
            id: (Utc::now() + chrono::Duration::minutes(30)).timestamp(),
            uses:  uses,
            expires: expires
        }
    }
    pub async fn add(&self, p: &Pool) -> Result<(), SqlError> {
        let conn = p.get_conn().await?;

        conn.prep_exec(
            "INSERT INTO invites (id, uses, expires)
            VALUES (:id, :uses, :expires)", params!{
                "id"    => self.id,
                "uses"  => self.uses,
                "expires" => self.expires
        }).await?;

        Ok(())
    }
}