use super::db::Database;
use super::db::DatabaseOperationError;
use super::db::{self};
use crate::models;
use redb::ReadableTable;
use std::collections::BTreeMap;

#[tracing::instrument(skip_all, err)]
pub async fn run_migrations(db: &Database) -> Result<(), Box<dyn std::error::Error>> {
    if is_at_version(db, 2).await? {
        migrate_to_v3(db).await?;
    }
    if is_at_version(db, 3).await? {
        migrate_to_v4(db).await?;
    }

    Ok(())
}

/// migrate: walls table
/// migrate: user table
/// remove: problems table
#[tracing::instrument(skip_all, err)]
pub async fn migrate_to_v4(db: &Database) -> Result<(), Box<dyn std::error::Error>> {
    tracing::warn!("MIGRATING TO VERSION 4");

    db.write(|txn| {
        let walls_dump = txn
            .open_table(db::v2::TABLE_WALLS)?
            .iter()?
            .map(|el| {
                let (k, v) = el.unwrap();
                (k.value(), v.value())
            })
            .collect::<BTreeMap<_, _>>();
        let problems_dump = txn
            .open_table(db::v2::TABLE_PROBLEMS)?
            .iter()?
            .map(|el| {
                let (k, v) = el.unwrap();
                (k.value(), v.value())
            })
            .collect::<BTreeMap<_, _>>();
        let user_dump = txn
            .open_table(db::v3::TABLE_USER)?
            .iter()?
            .map(|el| {
                let (k, v) = el.unwrap();
                (k.value(), v.value())
            })
            .collect::<BTreeMap<_, _>>();

        txn.delete_table(db::v2::TABLE_WALLS)?;
        txn.delete_table(db::v2::TABLE_PROBLEMS)?;
        txn.delete_table(db::v3::TABLE_USER)?;

        let mut new_walls_table = txn.open_table(db::current::TABLE_WALLS)?;
        let mut new_user_table = txn.open_table(db::current::TABLE_USER)?;

        for (wall_uid, wall) in walls_dump.into_iter() {
            let models::v2::Wall {
                uid: _,
                rows,
                cols,
                holds,
                problems,
            } = wall;

            let problems = problems
                .into_iter()
                .map(|problem_uid| {
                    let old_prob = &problems_dump[&(wall_uid, problem_uid)];
                    let method = old_prob.method;
                    let problem = models::Problem {
                        pattern: models::Pattern {
                            pattern: old_prob.holds.clone(),
                        },
                        method,
                    };
                    problem
                })
                .collect();
            let wall = models::Wall {
                uid: wall_uid,
                wall_dimensions: models::WallDimensions { rows, cols },
                holds,
                problems,
            };

            new_walls_table.insert(wall_uid, wall)?;
        }

        for ((wall_uid, problem_uid), user_interaction) in user_dump.into_iter() {
            let old_prob = &problems_dump[&(wall_uid, problem_uid)];
            let problem = models::Problem {
                pattern: models::Pattern {
                    pattern: old_prob.holds.clone(),
                },
                method: old_prob.method,
            };
            let key = (wall_uid, problem.clone());
            let value = models::UserInteraction {
                wall_uid,
                problem,
                attempted_on: user_interaction.attempted_on,
                is_favorite: user_interaction.is_favorite,
            };
            new_user_table.insert(key, value)?;
        }

        Ok(())
    })
    .await?;

    db.set_version(db::Version { version: db::v4::VERSION }).await?;

    Ok(())
}

#[tracing::instrument(skip_all, err)]
pub async fn migrate_to_v3(db: &Database) -> Result<(), Box<dyn std::error::Error>> {
    use redb::ReadableTableMetadata;
    tracing::warn!("MIGRATING TO VERSION 3");

    db.write(|txn| {
        // Opening the table creates the table
        let table = txn.open_table(db::current::TABLE_USER)?;
        assert!(table.is_empty()?);
        Ok(())
    })
    .await?;

    db.set_version(db::Version { version: db::v3::VERSION }).await?;

    Ok(())
}

async fn is_at_version(db: &Database, version: u64) -> Result<bool, DatabaseOperationError> {
    let v = db.get_version().await?;
    Ok(v == Some(db::Version { version }))
}
