use crate::commons::utils::CONFIG as config;
use crate::db::DbDriver;
use async_trait::async_trait;
use serde_json::{Value, json};
use sqlx::mysql::{MySqlArguments, MySqlPoolOptions, MySqlRow};
use sqlx::{Column, MySql, MySqlConnection, MySqlPool, Row, TypeInfo};

pub struct MysqlDb {
    pool: MySqlPool,
}

impl MysqlDb {
    pub async fn new() -> Result<Self, String> {
        let pool = MySqlPoolOptions::new()
            .after_connect(|conn: &mut MySqlConnection, _| {
                Box::pin(async move {
                    sqlx::query("SET time_zone = '+08:00'")
                        .execute(conn)
                        .await?;
                    Ok(())
                })
            })
            .max_connections(16)
            .connect(&config.db.clone().unwrap().uri)
            .await
            .map_err(|err| err.to_string())?;
        Ok(Self { pool })
    }
}

#[async_trait]
impl DbDriver for MysqlDb {
    async fn query(&self, sql: &str, params: Vec<Value>) -> Result<Vec<Value>, String> {
        let mut conn = self.pool.acquire().await.map_err(|err| err.to_string())?;
        mysql_query_rows(&mut *conn, sql, params).await
    }

    async fn transaction(&self, queries: Vec<Value>) -> Result<Vec<Value>, String> {
        let mut tx = self.pool.begin().await.map_err(|err| err.to_string())?;
        let mut results = Vec::with_capacity(queries.len());

        for query in queries.iter() {
            let sql = query
                .get("query")
                .or_else(|| query.get("sql"))
                .and_then(|value| value.as_str())
                .unwrap_or("")
                .trim();
            if sql.is_empty() {
                return Err("transaction query sql is empty".to_string());
            }

            let params = query
                .get("params")
                .and_then(|value| value.as_array())
                .cloned()
                .unwrap_or_default();
            let rows = mysql_query_rows(&mut *tx, sql, params).await?;
            results.push(Value::Array(rows));
        }

        tx.commit().await.map_err(|err| err.to_string())?;
        Ok(results)
    }
}

async fn mysql_query_rows<'e, E>(
    executor: E,
    sql: &str,
    params: Vec<Value>,
) -> Result<Vec<Value>, String>
where
    E: sqlx::Executor<'e, Database = MySql>,
{
    let sql = normalize_sql(sql);
    let mut query = sqlx::query(sql.as_str());
    for param in params.into_iter() {
        query = mysql_bind_param(query, param);
    }

    let rows = query
        .fetch_all(executor)
        .await
        .map_err(|err| err.to_string())?;
    rows.into_iter()
        .map(mysql_row_to_json)
        .collect::<Result<Vec<_>, _>>()
}

fn normalize_sql(sql: &str) -> String {
    sql.replace("?::date", "?")
        .replace("?::jsonb", "?")
        .replace("?::json", "?")
}

fn mysql_bind_param<'q>(
    query: sqlx::query::Query<'q, MySql, MySqlArguments>,
    value: Value,
) -> sqlx::query::Query<'q, MySql, MySqlArguments> {
    match value {
        Value::Bool(v) => query.bind(v),
        Value::Number(v) => {
            if let Some(v) = v.as_i64() {
                query.bind(v)
            } else {
                let v = v.as_f64().unwrap_or_default();
                if v.fract() == 0.0 {
                    query.bind(v as i64)
                } else {
                    query.bind(v)
                }
            }
        }
        Value::String(v) => query.bind(v),
        Value::Array(v) => query.bind(sqlx::types::Json(Value::Array(v))),
        Value::Object(v) => query.bind(sqlx::types::Json(Value::Object(v))),
        Value::Null => query.bind(Option::<String>::None),
    }
}

fn mysql_row_to_json(row: MySqlRow) -> Result<Value, String> {
    let mut item = serde_json::Map::new();
    for (index, column) in row.columns().iter().enumerate() {
        let type_name = column.type_info().name().to_ascii_uppercase();
        let value = match type_name.as_str() {
            "BOOLEAN" | "BOOL" => json!(
                row.try_get::<Option<bool>, _>(index)
                    .map_err(|err| err.to_string())?
            ),
            "TINYINT" => json!(
                row.try_get::<Option<i8>, _>(index)
                    .map_err(|err| err.to_string())?
            ),
            "SMALLINT" => json!(
                row.try_get::<Option<i16>, _>(index)
                    .map_err(|err| err.to_string())?
            ),
            "INT" | "INTEGER" | "MEDIUMINT" => json!(
                row.try_get::<Option<i32>, _>(index)
                    .map_err(|err| err.to_string())?
            ),
            "BIGINT" => json!(
                row.try_get::<Option<i64>, _>(index)
                    .map_err(|err| err.to_string())?
            ),
            "FLOAT" => json!(
                row.try_get::<Option<f32>, _>(index)
                    .map_err(|err| err.to_string())?
            ),
            "DOUBLE" | "REAL" => json!(
                row.try_get::<Option<f64>, _>(index)
                    .map_err(|err| err.to_string())?
            ),
            "VARCHAR" | "CHAR" | "TEXT" | "TINYTEXT" | "MEDIUMTEXT" | "LONGTEXT" => json!(
                row.try_get::<Option<String>, _>(index)
                    .map_err(|err| err.to_string())?
            ),
            "JSON" => row
                .try_get::<Option<sqlx::types::Json<Value>>, _>(index)
                .map_err(|err| err.to_string())?
                .map(|value| value.0)
                .unwrap_or(Value::Null),
            "DATE" => json!(
                row.try_get::<Option<chrono::NaiveDate>, _>(index)
                    .map_err(|err| err.to_string())?
                    .map(|date| date.to_string())
            ),
            "DATETIME" | "TIMESTAMP" => {
                let value = row
                    .try_get::<Option<chrono::NaiveDateTime>, _>(index)
                    .map_err(|err| err.to_string())?
                    .map(|ts| ts.and_utc().timestamp_millis())
                    .unwrap_or(0);
                json!(value)
            }
            _ => json!(type_name),
        };
        item.insert(column.name().to_string(), value);
    }
    Ok(Value::Object(item))
}
