use crate::commons::utils::CONFIG as config;
use crate::db::DbDriver;
use async_trait::async_trait;
use serde_json::{Value, json};
use sqlx::postgres::{PgPoolOptions, PgRow};
use sqlx::{Column, PgPool, Row, TypeInfo};

pub struct PgDb {
    pool: PgPool,
}

impl PgDb {
    pub async fn new() -> Result<Self, String> {
        let pool = PgPoolOptions::new()
            .max_connections(16)
            .connect(&config.db.clone().unwrap().uri)
            .await
            .map_err(|err| err.to_string())?;

        Ok(Self { pool })
    }
}

#[async_trait]
impl DbDriver for PgDb {
    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())?;
        pg_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 = pg_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 pg_query_rows<'e, E>(
    executor: E,
    sql: &str,
    params: Vec<Value>,
) -> Result<Vec<Value>, String>
where
    E: sqlx::Executor<'e, Database = sqlx::Postgres>,
{
    let sql = normalize_sql_placeholders(sql);
    let mut query = sqlx::query(sql.as_str());
    for param in params.into_iter() {
        query = pg_bind_param(query, param);
    }

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

fn normalize_sql_placeholders(sql: &str) -> String {
    let mut index = 1;
    let mut out = String::with_capacity(sql.len());
    for ch in sql.chars() {
        if ch == '?' {
            out.push('$');
            out.push_str(index.to_string().as_str());
            index += 1;
        } else {
            out.push(ch);
        }
    }
    out
}

fn pg_bind_param<'q>(
    query: sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments>,
    value: Value,
) -> sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments> {
    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(Value::Array(v)),
        Value::Object(v) => query.bind(Value::Object(v)),
        Value::Null => query.bind(Option::<String>::None),
    }
}

fn pg_row_to_json(row: PgRow) -> 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() {
            "BOOL" => json!(
                row.try_get::<Option<bool>, _>(index)
                    .map_err(|err| err.to_string())?
            ),
            "INT2" => json!(
                row.try_get::<Option<i16>, _>(index)
                    .map_err(|err| err.to_string())?
            ),
            "INT4" => json!(
                row.try_get::<Option<i32>, _>(index)
                    .map_err(|err| err.to_string())?
            ),
            "INT8" => json!(
                row.try_get::<Option<i64>, _>(index)
                    .map_err(|err| err.to_string())?
            ),
            "FLOAT4" => {
                json!(
                    row.try_get::<Option<f32>, _>(index)
                        .map_err(|err| err.to_string())?
                )
            }
            "FLOAT8" => {
                json!(
                    row.try_get::<Option<f64>, _>(index)
                        .map_err(|err| err.to_string())?
                )
            }
            "VARCHAR" | "TEXT" | "BPCHAR" | "NAME" => {
                json!(
                    row.try_get::<Option<String>, _>(index)
                        .map_err(|err| err.to_string())?
                )
            }
            "JSON" | "JSONB" => row
                .try_get::<Option<Value>, _>(index)
                .map_err(|err| err.to_string())?
                .unwrap_or(Value::Null),
            "TIMESTAMPTZ" => {
                let value = row
                    .try_get::<Option<chrono::DateTime<chrono::Utc>>, _>(index)
                    .map_err(|err| err.to_string())?
                    .map(|ts| ts.timestamp_millis())
                    .unwrap_or(0);
                json!(value)
            }
            _ => json!(type_name),
        };
        item.insert(column.name().to_string(), value);
    }
    Ok(Value::Object(item))
}
