use std::collections;
use std::collections::HashMap;
use std::error::Error;
use std::ops::Add;
use std::sync::{Arc, Mutex};

use chrono::{TimeZone, Utc};
use mysql::{Binary, Pool, ResultSet, Value};
use mysql::prelude::Queryable;

use crate::data::config::DatabaseConfig;
use crate::data::error::{ExecError, ResultBoxError};
use crate::data::user::UserID;

/// Database access helper
///
/// @author Pierre Hubert

pub type ProcessRowResult<E> = Result<E, Box<dyn Error>>;

// Pool shared across threads
static mut POOL: Option<Arc<Mutex<mysql::Pool>>> = None;

/// Connect to the database
pub fn connect(conf: &DatabaseConfig) -> Result<(), Box<dyn Error>> {
    let url = format!(
        "mysql://{}:{}@{}:3306/{}",
        conf.username, conf.password, conf.host, conf.name
    );

    let pool = Pool::new(url)?;
    let pool = Some(Arc::new(Mutex::new(pool)));

    unsafe {
        POOL = pool;
    }

    Ok(())
}

/// Get a connection to the database
pub fn get_connection() -> Result<mysql::PooledConn, Box<dyn Error>> {
    let pool: Pool;

    unsafe {
        let guard = POOL.as_ref().unwrap().lock();
        pool = guard.unwrap().clone()
    }

    Ok(pool.get_conn()?)
}

/// Structure used to implement JOIN on queries
struct QueryJoin {
    table: String,
    table_alias: String,
    condition: String,
}

pub struct QueryInfo {
    /// Fetched table
    pub table: String,
    pub table_alias: Option<String>,

    /// Joins
    joins: Vec<QueryJoin>,

    /// Query limits
    pub conditions: collections::HashMap<String, String>,

    /// Custom WHERE condition
    pub custom_where: Option<String>,

    /// Custom WHERE values
    pub custom_where_ars: Vec<mysql::Value>,

    /// Limit of the query (0 = no limit)
    pub limit: u64,

    /// Order of the query
    pub order: Option<String>,

    /// Queried arguments
    ///
    /// If this attribute is empty, all the columns of the table set are fetched
    pub fields: Vec<String>,
}

impl QueryInfo {
    /// Initialize a new query on the database
    pub fn new(table: &str) -> QueryInfo {
        QueryInfo {
            table: table.to_string(),
            table_alias: None,
            joins: Vec::new(),
            conditions: collections::HashMap::new(),
            custom_where: None,
            custom_where_ars: vec![],
            limit: 0,
            order: None,
            fields: Vec::new(),
        }
    }

    /// Main fetched table alias
    pub fn alias(mut self, table_alias: &str) -> QueryInfo {
        self.table_alias = Some(table_alias.to_string());
        self
    }

    pub fn join(mut self, table: &str, table_alias: &str, cond: &str) -> QueryInfo {
        self.joins.push(QueryJoin {
            table: table.to_string(),
            table_alias: table_alias.to_string(),
            condition: cond.to_string(),
        });
        self
    }

    pub fn cond(mut self, key: &str, val: &str) -> QueryInfo {
        self.conditions.insert(key.to_string(), val.to_string());
        self
    }

    pub fn cond_u32(mut self, key: &str, val: u32) -> QueryInfo {
        self.conditions.insert(key.to_string(), val.to_string());
        self
    }

    pub fn cond_u64(mut self, key: &str, val: u64) -> QueryInfo {
        self.conditions.insert(key.to_string(), val.to_string());
        self
    }

    pub fn cond_i64(mut self, key: &str, val: i64) -> QueryInfo {
        self.conditions.insert(key.to_string(), val.to_string());
        self
    }

    pub fn cond_user_id(mut self, key: &str, val: UserID) -> QueryInfo {
        self.conditions.insert(key.to_string(), val.to_string());
        self
    }

    /// Set custom where
    pub fn set_custom_where(mut self, custom_where: &str) -> QueryInfo {
        self.custom_where = Some(custom_where.to_string());
        self
    }

    /// Add a custom u64 WHERE value
    pub fn add_custom_where_argument_u64(mut self, val: u64) -> QueryInfo {
        self.custom_where_ars.push(mysql::Value::UInt(val));
        self
    }

    /// Append a field to the list of selected fields
    pub fn add_field(mut self, key: &str) -> QueryInfo {
        self.fields.push(key.to_string());
        self
    }

    /// Set the limit for the request
    pub fn set_limit(mut self, value: u64) -> QueryInfo {
        self.limit = value;
        self
    }

    /// Set results ordering
    pub fn set_order(mut self, order: &str) -> QueryInfo {
        self.order = Some(order.to_string());
        self
    }

    /// Execute query
    pub fn exec<E, F: Fn(&RowResult) -> ProcessRowResult<E>>(self, process_function: F)
                                                             -> Result<Vec<E>, Box<dyn Error>> {
        query(self, process_function)
    }

    /// Query just a row
    pub fn query_row<E, F: Fn(&RowResult) -> ProcessRowResult<E>>(self, process_function: F)
                                                                  -> Result<E, Box<dyn Error>> {
        query_row(self, process_function)
    }

    /// Execute count query
    pub fn exec_count(self) -> ResultBoxError<usize> {
        count(self)
    }
}

/// Struct used to read the result of a request
pub struct RowResult<'a> {
    row: &'a mysql::Row
}

impl<'a> RowResult<'a> {
    pub fn new(row: &mysql::Row) -> RowResult {
        RowResult {
            row
        }
    }

    /// Find a column in result set
    fn find_col(&self, name: &str) -> Result<usize, ExecError> {
        let name_bytes = name.as_bytes();
        let mut index = 0;
        for c in self.row.columns_ref() {
            if c.name_ref().eq(name_bytes) {
                return Ok(index);
            }

            index += 1;
        }

        Err(ExecError(format!("Column {} not found in database result set!", name)))
    }

    /// Find an integer included in the request
    pub fn get_int64(&self, name: &str) -> Result<i64, Box<dyn Error>> {
        let value = self.row.get_opt(self.find_col(name)?);

        match value {
            None => Err(ExecError::boxed_string(
                format!("Could not extract integer field {} !", name))),
            Some(s) => Ok(s?)
        }
    }

    /// Find an integer included in the request
    pub fn get_u64(&self, name: &str) -> Result<u64, Box<dyn Error>> {
        let value = self.row.get_opt(self.find_col(name)?);

        match value {
            None => Err(ExecError::boxed_string(
                format!("Could not extract integer field {} !", name))),
            Some(s) => Ok(s?)
        }
    }

    /// Find an integer included in the request
    pub fn get_usize(&self, name: &str) -> Result<usize, Box<dyn Error>> {
        let value = self.row.get_opt(self.find_col(name)?);

        match value {
            None => Err(ExecError::boxed_string(
                format!("Could not extract integer field {} !", name))),
            Some(s) => Ok(s?)
        }
    }

    /// Get the ID of a user included in the request
    pub fn get_user_id(&self, name: &str) -> ResultBoxError<UserID> {
        self.get_int64(name)
    }

    /// Find a string included in the request
    pub fn get_str(&self, name: &str) -> Result<String, Box<dyn Error>> {
        let value = self.row.get_opt(self.find_col(name)?);

        match value {
            None => Err(ExecError::boxed_string(
                format!("Could not extract string field {} !", name))),
            Some(s) => Ok(s?)
        }
    }

    /// Check out whether a given value is null or not
    pub fn is_null(&self, name: &str) -> ResultBoxError<bool> {
        let value = self.row.get_opt(self.find_col(name)?);

        match value {
            None => Ok(true),
            Some(Ok(Value::NULL)) => Ok(true),
            _ => Ok(false)
        }
    }

    /// Get an optional string => Set to None if string is null / empty
    pub fn get_optional_str(&self, name: &str) -> ResultBoxError<Option<String>> {
        match self.is_null(name)? {
            true => Ok(None),
            false => Ok(Some(self.get_str(name)?).map_or(None, |d| {
                match d.is_empty() {
                    true => None,
                    false => Some(d)
                }
            }))
        }
    }

    /// Get legacy boolean value : 1 = true / 0 = false
    pub fn get_legacy_bool(&self, name: &str) -> ResultBoxError<bool> {
        Ok(self.get_int64(name)? == 1)
    }

    /// Get a MYSQL date as a timestamp
    pub fn get_date_as_time(&self, name: &str) -> ResultBoxError<u64> {
        let value = self.row.get_opt(self.find_col(name)?);
        let value: Value = value.ok_or(ExecError(format!("Could not find date field {} !", name)))??;

        // Check if it is a date
        if let Value::Date(year, month, day, hour, minutes, seconds, mic_secs) = value {
            let dt = Utc.ymd(year as i32, month as u32, day as u32)
                .and_hms_micro(hour as u32, minutes as u32, seconds as u32, mic_secs);

            return Ok(dt.timestamp() as u64);
        }

        Err(ExecError::boxed_string(format!("Field {} could not be resolved as a date !", name)))
    }
}


/// Query a single row of the database
pub fn query_row<E, F: Fn(&RowResult) -> ProcessRowResult<E>>(mut info: QueryInfo,
                                                              process_function: F) -> Result<E, Box<dyn Error>> {
    let table = info.table.clone();

    info.limit = 1;
    let mut list = query(info, process_function)?;

    match list.len() {
        0 => Err(ExecError::boxed_string(
            format!("Database query did not return a result: (table: {}) !", table))),
        _ => Ok(list.remove(0))
    }
}

/// Make a simple query
pub fn query<E, F: Fn(&RowResult) -> ProcessRowResult<E>>(info: QueryInfo, process_function: F)
                                                          -> Result<Vec<E>, Box<dyn Error>> {
    let mut params = vec![];

    let select_elements = match info.fields.len() {
        0 => "*".to_string(),
        _ => info.fields.join(", ")
    };

    // Build query
    let mut query = format!("SELECT {} FROM {} ", select_elements, info.table);

    // Table alias (if any)
    if let Some(alias) = info.table_alias {
        query = query.add(format!(" {} ", alias).as_str());
    }

    // Join conditions
    for j in info.joins {
        query = query.add(
            format!(" JOIN {} {} ON {} ", j.table, j.table_alias, j.condition).as_ref());
    }


    // WHERE clause
    if !info.conditions.is_empty() {
        let mut where_args = vec![];

        for (k, v) in info.conditions {
            where_args.push(format!("{} = ?", k));
            params.push(mysql::Value::from(v));
        }

        let where_args = format!(" WHERE {} ", where_args.join(" AND "));
        query = query.add(&where_args);
    }

    // Custom WHERE clause
    if let Some(custom_where) = info.custom_where {
        query = query.add(format!(" AND ({})", custom_where).as_str());

        let mut custom_args = info.custom_where_ars;
        params.append(&mut custom_args);
    }

    // ORDER clause
    if let Some(order) = info.order {
        query = query.add(format!(" ORDER BY {} ", order).as_str());
    }

    // LIMIT clause
    if info.limit > 0 {
        query = query.add(&format!(" LIMIT {}", info.limit));
    }

    // Execute query
    let mut con = get_connection()?;
    let stmt = con.prep(&query).or_else(|err| {
        println!("Error in SQL query: {}", &query);
        Err(err)
    })?;

    let mut res = con.exec_iter(stmt, params)?;

    // This system is made to support only one dataset
    let result_set = res.next_set();
    if let None = result_set {
        return Err(Box::new(ExecError::new("No result set in a query!")));
    }
    let result_set: ResultSet<Binary> = result_set.unwrap()?;


    // Parse each result of the dataset
    let mut res: Vec<E> = vec![];
    for row in result_set {
        let row = row?;
        let result = RowResult::new(&row);

        let parsed = process_function(&result)?;
        res.push(parsed);
    }


    Ok(res)
}

/// Count the number of results a query would have produced
pub fn count(mut q: QueryInfo) -> ResultBoxError<usize> {
    &q.fields.clear();
    &q.fields.push("COUNT(*) as count".to_string());

    query(q, |res| res.get_usize("count"))?.pop()
        .ok_or(ExecError::boxed_new("database::count did not return a result!"))
}

/// Structure used to execute a insert query
pub struct InsertQuery {
    pub table: String,
    pub values: HashMap<String, mysql::Value>,
}

impl InsertQuery {
    /// Construct a new InsertQuery instance
    pub fn new(table: &str) -> InsertQuery {
        InsertQuery {
            table: table.to_string(),
            values: HashMap::new(),
        }
    }

    /// Add a string
    pub fn add_str(mut self, key: &str, value: &str) -> InsertQuery {
        self.values.insert(key.to_string(), Value::from(value));
        self
    }

    /// Add an optional string. If None, an empty string will be inserted
    pub fn add_opt_str(mut self, key: &str, value: Option<&String>) -> InsertQuery {
        self.values.insert(key.to_string(), Value::from(value.unwrap_or(&String::new())));
        self
    }

    /// Add an integer
    pub fn add_i64(mut self, key: &str, value: i64) -> InsertQuery {
        self.values.insert(key.to_string(), Value::from(value));
        self
    }

    pub fn add_u64(mut self, key: &str, value: u64) -> InsertQuery {
        self.values.insert(key.to_string(), Value::from(value));
        self
    }

    pub fn add_u32(mut self, key: &str, value: u32) -> InsertQuery {
        self.values.insert(key.to_string(), Value::from(value));
        self
    }

    pub fn add_user_id(mut self, key: &str, value: UserID) -> InsertQuery {
        self.values.insert(key.to_string(), Value::from(value));
        self
    }

    /// Legacy database boolean (1 = true / 0 = false)
    pub fn add_legacy_bool(mut self, key: &str, value: bool) -> InsertQuery {
        let num = match value {
            true => 1,
            false => 0
        };

        self.values.insert(key.to_string(), Value::from(num));
        self
    }

    /// Process insert
    pub fn insert(self) -> ResultBoxError<Option<u64>> {
        insert(self)
    }
}

/// Insert a new entry into the database
pub fn insert(query: InsertQuery) -> ResultBoxError<Option<u64>> {

    // Collect keys
    let keys = query.values
        .keys()
        .map(|f| f.to_string())
        .collect::<Vec<String>>();

    let query_sql = format!(
        "INSERT INTO {} ({}) VALUES({})",
        query.table,
        keys.join(", "),
        keys.iter().map(|_| "?").collect::<Vec<&str>>().join(", ")
    );

    let mut con = get_connection()?;
    let res = con.exec_iter(
        query_sql,
        query.values.values().collect::<Vec<&mysql::Value>>(),
    )?;

    Ok(res.last_insert_id())
}


/// Structure used to build delete queries
pub struct DeleteQuery {
    table: String,
    conditions: HashMap<String, Value>,
}

impl DeleteQuery {
    /// Construct a new delete table query
    pub fn new(table: &str) -> DeleteQuery {
        DeleteQuery {
            table: table.to_string(),
            conditions: HashMap::new(),
        }
    }

    /// Add a string condition
    pub fn cond_str(mut self, key: &str, value: &str) -> DeleteQuery {
        self.conditions.insert(key.to_string(), Value::from(value));
        self
    }

    /// Add an integer condition
    pub fn cond_u32(mut self, key: &str, value: u32) -> DeleteQuery {
        self.conditions.insert(key.to_string(), Value::from(value));
        self
    }

    pub fn cond_i64(mut self, key: &str, value: i64) -> DeleteQuery {
        self.conditions.insert(key.to_string(), Value::from(value));
        self
    }

    pub fn cond_u64(mut self, key: &str, value: u64) -> DeleteQuery {
        self.conditions.insert(key.to_string(), Value::from(value));
        self
    }

    pub fn cond_user_id(mut self, key: &str, value: UserID) -> DeleteQuery {
        self.conditions.insert(key.to_string(), Value::from(value));
        self
    }

    /// Execute the delete query
    pub fn exec(self) -> ResultBoxError<()> {
        delete(self)
    }
}

/// Delete an entry from the database
pub fn delete(query: DeleteQuery) -> ResultBoxError<()> {
    if query.conditions.is_empty() {
        return Err(ExecError::boxed_new("DELETE without WHERE condition blocked for security reasons!"));
    }

    let query_sql = format!(
        "DELETE FROM {} WHERE {}",
        query.table,
        query.conditions.keys()
            .map(|f| format!("{} = ?", f))
            .collect::<Vec<String>>()
            .join(" AND ")
    );

    get_connection()?.exec_drop(
        query_sql,
        query.conditions.values().collect::<Vec<&Value>>(),
    )?;

    Ok(())
}

pub struct UpdateInfo {
    table: String,
    cond: HashMap<String, Value>,
    custom_where: Option<String>,
    custom_where_args: Vec<mysql::Value>,
    set: HashMap<String, Value>,
}

impl UpdateInfo {
    /// Prepare a new update query
    pub fn new(table: &str) -> UpdateInfo {
        UpdateInfo {
            table: table.to_string(),
            cond: HashMap::new(),
            custom_where: None,
            custom_where_args: vec![],
            set: HashMap::new(),
        }
    }

    /// Filter with a user id
    pub fn cond_user_id(mut self, name: &str, val: UserID) -> UpdateInfo {
        self.cond.insert(name.to_string(), Value::Int(val));
        self
    }

    /// Filter with an unsigned integer
    pub fn cond_u64(mut self, name: &str, val: u64) -> UpdateInfo {
        self.cond.insert(name.to_string(), Value::UInt(val));
        self
    }

    /// Filter with a legacy boolean
    pub fn cond_legacy_bool(mut self, name: &str, val: bool) -> UpdateInfo {
        let num = match val {
            true => 1,
            false => 0
        };

        self.cond.insert(name.to_string(), Value::Int(num));
        self
    }

    /// Add custom WHERE clause
    pub fn custom_where(mut self, query: &str) -> UpdateInfo {
        self.custom_where = Some(query.to_string());
        self
    }

    /// Add custom WHERE argument
    pub fn add_custom_where_arg_u64(mut self, arg: u64) -> UpdateInfo {
        self.custom_where_args.push(mysql::Value::UInt(arg));
        self
    }

    /// Set an new optional string
    ///
    /// None => Empty string
    /// Some => The string
    pub fn set_opt_str(mut self, name: &str, val: Option<String>) -> UpdateInfo {
        self.set.insert(name.to_string(), Value::from(val.unwrap_or(String::new())));
        self
    }

    /// Set a new legacy boolean
    pub fn set_legacy_bool(mut self, name: &str, val: bool) -> UpdateInfo {
        let num = match val {
            false => 0,
            true => 1,
        };

        self.set.insert(name.to_string(), Value::Int(num));
        self
    }

    /// Set a new unsigned number
    pub fn set_u64(mut self, name: &str, val: u64) -> UpdateInfo {
        self.set.insert(name.to_string(), Value::UInt(val));
        self
    }

    /// Execute the update
    pub fn exec(self) -> ResultBoxError<()> {
        update(self)
    }
}

/// Execute an update query
pub fn update(u: UpdateInfo) -> ResultBoxError<()> {
    if u.cond.is_empty() {
        Err(ExecError::boxed_new("Update without conditions blocked for security!"))?;
    }

    if u.set.is_empty() {
        Err(ExecError::boxed_new("Update with no change specified useless!"))?;
    }


    let mut query_sql = format!("UPDATE {} SET ", u.table);
    let mut values = vec![];

    // Prepare updates
    let updates = u.set.keys()
        .into_iter()
        .map(|f| format!("{} = ?", f))
        .collect::<Vec<String>>()
        .join(", ");
    u.set.values().into_iter().for_each(|f| values.push(f));

    // Prepare conditions
    let mut conditions = u.cond.keys()
        .into_iter()
        .map(|f| format!("{} = ?", f))
        .collect::<Vec<String>>()
        .join(" AND ");
    u.cond.values().into_iter().for_each(|f| values.push(f));

    // Additional conditions
    if let Some(custom_where) = u.custom_where {
        conditions = format!("({}) AND ({})", conditions, custom_where);

        u.custom_where_args.iter().for_each(|f| values.push(f))
    }

    query_sql = format!("{} {} WHERE {}", query_sql, updates, conditions);

    get_connection()?.exec_drop(
        query_sql,
        values,
    )?;

    Ok(())
}