use deadpool_postgres::GenericClient;

use super::QueryError;
use super::QueryResult;
use crate::entity::Entity;
use crate::entity::VersionedEntity;
use crate::query::QueryBindings;

pub async fn insert_entity<E: Entity>(db: &impl GenericClient, entity: &E) -> QueryResult<()> {
    let table = E::TABLE_NAME;
    let cols = E::COLUMN_NAMES.join(", ");

    let mut bindings = QueryBindings::new();
    entity.bind_db_params(&mut bindings);
    let param_list = build_param_list(1, E::COLUMN_NAMES.len());

    let query = format!("INSERT INTO {table} ({cols}) VALUES ({param_list})");

    let num_rows_affected = db.execute(&query, bindings.as_ref()).await?;
    check_row_count(1, num_rows_affected)?;

    return Ok(());
}

pub async fn bulk_insert_entities<E: Entity>(
    db: &impl GenericClient,
    entities: &[&E],
) -> QueryResult<()> {
    let table = E::TABLE_NAME;
    let cols = E::COLUMN_NAMES.join(", ");
    let num_cols = E::COLUMN_NAMES.len();

    let mut bindings = QueryBindings::new();
    let mut param_lists = Vec::with_capacity(entities.len());

    for (i, entity) in entities.iter().enumerate() {
        entity.bind_db_params(&mut bindings);
        let list = build_param_list(num_cols * i + 1, num_cols * (i + 1));
        param_lists.push(format!("({list})"));
    }

    let param_lists = param_lists.join(", ");
    let query = format!("INSERT INTO {table} ({cols}) VALUES {param_lists}");

    let num_rows_affected = db.execute(&query, bindings.as_ref()).await?;
    check_row_count(entities.len(), num_rows_affected)?;

    return Ok(());
}

pub async fn update_entity<E: Entity>(db: &impl GenericClient, entity: &E) -> QueryResult<()> {
    let table = E::TABLE_NAME;
    let id_col = E::COLUMN_NAMES[E::ID_COLUMN_INDEX];

    let mut bindings = QueryBindings::new();
    bindings.add(entity.id()); // $1

    // TODO: Figure out how to do field diffs (?)
    entity.bind_db_params(&mut bindings); // $2..

    // TODO: Find a way to avoid recomputing this everytime
    let assigns = (2..)
        .zip(E::COLUMN_NAMES)
        .map(|(n, c)| format!("{c} = ${n}"));
    let assigns = assigns.collect::<Vec<_>>().join(", ");

    let query = format!("UPDATE {table} SET {assigns} WHERE {id_col} = $1");
    let num_rows_affected = db.execute(&query, bindings.as_ref()).await?;
    check_row_count(1, num_rows_affected)?;

    return Ok(());
}

pub async fn update_entity_versioned<E: VersionedEntity>(
    db: &impl GenericClient,
    entity: &E,
) -> QueryResult<()> {
    let table = E::TABLE_NAME;
    let id_col = E::COLUMN_NAMES[E::ID_COLUMN_INDEX];
    let version_col = E::COLUMN_NAMES[E::VERSION_COLUMN_INDEX];

    let prev_version = entity.version() - 1;

    let mut params = QueryBindings::new();
    params.add(entity.id()); // $1
    params.add(&prev_version); // $2

    // TODO: Figure out how to do field diffs (?)
    entity.bind_db_params(&mut params); // $3...

    // TODO: Find a way to avoid recomputing this everytime
    let assigns = (3..)
        .zip(E::COLUMN_NAMES)
        .map(|(n, c)| format!("{c} = ${n}"));
    let assigns = assigns.collect::<Vec<_>>().join(", ");

    let query = format!("UPDATE {table} SET {assigns} WHERE {id_col} = $1 AND {version_col} = $2");
    let num_rows_affected = db.execute(&query, params.as_ref()).await?;
    check_row_count(1, num_rows_affected)?;

    return Ok(());
}

pub async fn delete_entity<E: Entity>(db: &impl GenericClient, entity: &E) -> QueryResult<()> {
    let table = E::TABLE_NAME;
    let id_col = E::COLUMN_NAMES[E::ID_COLUMN_INDEX];

    let mut bindings = QueryBindings::new();
    bindings.add(entity.id()); // $1

    let query = format!("DELETE FROM {table} WHERE {id_col} = $1");
    let num_rows_affected = db.execute(&query, bindings.as_ref()).await?;
    check_row_count(1, num_rows_affected)?;

    return Ok(());
}

pub async fn delete_entity_versioned<E: VersionedEntity>(
    db: &impl GenericClient,
    entity: &E,
) -> QueryResult<()> {
    let table = E::TABLE_NAME;
    let id_col = E::COLUMN_NAMES[E::ID_COLUMN_INDEX];
    let version_col = E::COLUMN_NAMES[E::VERSION_COLUMN_INDEX];

    let version = entity.version();

    let mut bindings = QueryBindings::new();
    bindings.add(entity.id()); // $1
    bindings.add(&version); // $2

    let query = format!("DELETE FROM {table} WHERE {id_col} = $1 AND {version_col} = $2");
    let num_rows_affected = db.execute(&query, bindings.as_ref()).await?;
    check_row_count(1, num_rows_affected)?;

    return Ok(());
}

pub async fn bulk_delete_entities<E: VersionedEntity>(
    db: &impl GenericClient,
    entities: &[&E],
) -> QueryResult<()> {
    let table = E::TABLE_NAME;
    let id_col = E::COLUMN_NAMES[E::ID_COLUMN_INDEX];

    let id_param_list = build_param_list(1, entities.len());
    let mut bindings = QueryBindings::new();
    for e in entities {
        bindings.add(e.id());
    }

    let query = format!("DELETE FROM {table} WHERE {id_col} IN ({id_param_list})");
    let num_rows_affected = db.execute(&query, bindings.as_ref()).await?;
    check_row_count(entities.len(), num_rows_affected)?;

    return Ok(());
}

fn build_param_list(start: usize, end_inclusive: usize) -> String {
    // TODO: Make this more efficient by slicing a long pre-computed list (returning &'static str)
    let params = (start..=end_inclusive).map(|n| format!("${n}"));

    return params.collect::<Vec<_>>().join(", ");
}

fn check_row_count(expected: usize, actual: u64) -> QueryResult<()> {
    return if (actual as usize) == expected {
        Ok(())
    } else {
        Err(QueryError::UnexpectedRowCount { expected, actual })
    };
}
