use deadpool_postgres::GenericClient;
use std::collections::hash_map::Entry;
use std::collections::HashMap;

use super::QueryError;
use super::QueryResult;
use crate::entity::Entity;
use crate::entity::EntityColumn;
use crate::entity::EntityDiff;
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_full(&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.to_params()).await?;
    check_row_count(1, num_rows_affected)?;

    return Ok(());
}

pub async fn insert_entity_bulk<E: Entity>(
    db: &impl GenericClient,
    entities: &[&E],
) -> QueryResult<()> {
    if entities.is_empty() {
        return Ok(());
    }

    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());

    // TODO: Run in batches of <10k parameters eachers

    for (i, entity) in entities.iter().enumerate() {
        entity.bind_db_params_full(&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.to_params()).await?;
    check_row_count(entities.len(), num_rows_affected)?;

    return Ok(());
}

pub async fn update_entity<E: Entity>(
    db: &impl GenericClient,
    before_entity: &E,
    after_entity: &E,
) -> QueryResult<()> {
    let diff = E::diff_entity(before_entity, after_entity);
    let has_change = diff.flags.iter().any(|&f| f);
    if !has_change {
        return Ok(());
    }

    // TODO: Merge the two versions into one function
    return match E::VERSION_NUM_COLUMN {
        Some(col) => update_entity_partial_with_version_num(db, after_entity, col, &diff).await,
        None => update_entity_partial_simple(db, after_entity, &diff).await,
    };
}

async fn update_entity_partial_simple<E: Entity>(
    db: &impl GenericClient,
    entity: &E,
    diff: &EntityDiff<E>,
) -> QueryResult<()> {
    let table = E::TABLE_NAME;
    let id_col = E::ID_COLUMN.name;

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

    entity.bind_db_params_partial(&mut bindings, diff); // $2..

    let columns = E::COLUMN_NAMES
        .iter()
        .zip(&diff.flags)
        .filter_map(|(&c, f)| f.then_some(c));

    let assigns = (2..).zip(columns).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.to_params()).await?;
    check_row_count(1, num_rows_affected)?;

    return Ok(());
}

async fn update_entity_partial_with_version_num<E: Entity>(
    db: &impl GenericClient,
    entity: &E,
    veresion_num_col: &'static EntityColumn,
    diff: &EntityDiff<E>,
) -> QueryResult<()> {
    let table = E::TABLE_NAME;
    let id_col = E::ID_COLUMN.name;
    let version_num_col = veresion_num_col.name;

    let prev_version = entity.version() - 1;

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

    entity.bind_db_params_partial(&mut bindings, diff); // $3...

    let columns = E::COLUMN_NAMES
        .iter()
        .zip(&diff.flags)
        .filter_map(|(&c, f)| f.then_some(c));

    let assigns = (3..).zip(columns).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_num_col} = $2");
    let num_rows_affected = db.execute(&query, &bindings.to_params()).await?;
    check_row_count(1, num_rows_affected)?;

    return Ok(());
}

pub async fn update_entity_full<E: Entity>(db: &impl GenericClient, entity: &E) -> QueryResult<()> {
    // TODO: Consider implementing this using the diff functions instead
    // TODO: Merge the two versions into one function
    return match E::VERSION_NUM_COLUMN {
        Some(col) => update_entity_full_with_version_num(db, entity, col).await,
        None => update_entity_full_simple(db, entity).await,
    };
}

async fn update_entity_full_simple<E: Entity>(
    db: &impl GenericClient,
    entity: &E,
) -> QueryResult<()> {
    let table = E::TABLE_NAME;
    let id_col = E::ID_COLUMN.name;

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

    entity.bind_db_params_full(&mut bindings); // $2..

    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.to_params()).await?;
    check_row_count(1, num_rows_affected)?;

    return Ok(());
}

async fn update_entity_full_with_version_num<E: Entity>(
    db: &impl GenericClient,
    entity: &E,
    veresion_num_col: &'static EntityColumn,
) -> QueryResult<()> {
    let table = E::TABLE_NAME;
    let id_col = E::ID_COLUMN.name;
    let version_num_col = veresion_num_col.name;

    let prev_version = entity.version() - 1;

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

    entity.bind_db_params_full(&mut bindings); // $3...

    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_num_col} = $2");
    let num_rows_affected = db.execute(&query, &bindings.to_params()).await?;
    check_row_count(1, num_rows_affected)?;

    return Ok(());
}

pub async fn update_entity_bulk<E: Entity>(
    db: &impl GenericClient,
    before_entities: &[&E],
    after_entities: &[&E],
) -> QueryResult<()> {
    let mut create_list = Vec::<&E>::new();
    let mut update_list = Vec::<(&E, &E)>::new();

    let mut before_map = HashMap::<_, _>::from_iter(before_entities.iter().map(|&e| (e.id(), e)));

    for &ae in after_entities {
        if let Entry::Occupied(o) = before_map.entry(ae.id()) {
            let be = o.remove();
            update_list.push((be, ae));
        } else {
            create_list.push(ae);
        }
    }

    insert_entity_bulk(db, &create_list).await?;

    for (be, ae) in update_list {
        update_entity(db, be, ae).await?;
    }

    let delete_list: Vec<&E> = before_map.into_values().collect();
    delete_entity_bulk(db, &delete_list).await?;

    return Ok(());
}

pub async fn delete_entity<E: Entity>(db: &impl GenericClient, entity: &E) -> QueryResult<()> {
    // TODO: Merge the two versions into one function
    return match E::VERSION_NUM_COLUMN {
        Some(col) => delete_entity_with_version_num(db, entity, col).await,
        None => delete_entity_simple(db, entity).await,
    };
}

async fn delete_entity_simple<E: Entity>(db: &impl GenericClient, entity: &E) -> QueryResult<()> {
    let table = E::TABLE_NAME;
    let id_col = E::ID_COLUMN.name;

    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.to_params()).await?;
    check_row_count(1, num_rows_affected)?;

    return Ok(());
}

async fn delete_entity_with_version_num<E: Entity>(
    db: &impl GenericClient,
    entity: &E,
    veresion_num_col: &'static EntityColumn,
) -> QueryResult<()> {
    let table = E::TABLE_NAME;
    let id_col = E::ID_COLUMN.name;
    let version_num_col = veresion_num_col.name;

    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_num_col} = $2");
    let num_rows_affected = db.execute(&query, &bindings.to_params()).await?;
    check_row_count(1, num_rows_affected)?;

    return Ok(());
}

pub async fn delete_entity_bulk<E: Entity>(
    db: &impl GenericClient,
    entities: &[&E],
) -> QueryResult<()> {
    if entities.is_empty() {
        return Ok(());
    }

    let table = E::TABLE_NAME;
    let id_col = E::ID_COLUMN.name;

    // TODO: Run in batches of <10k parameters eachers

    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.to_params()).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 usize::try_from(actual).unwrap() == expected {
        Ok(())
    } else {
        Err(QueryError::UnexpectedRowCount { expected, actual })
    };
}
