use super::state::PersistentState;
use leptos::prelude::StorageAccess;
use redb::Database;
use redb::ReadableTable;
use redb::ReadableTableMetadata;
use std::collections::BTreeSet;
use std::path::PathBuf;
use type_toppings::ResultExt;

#[tracing::instrument]
pub async fn run_migrations(db: &Database) -> Result<(), Box<dyn std::error::Error>> {
    migrate_state_file().await?;
    migrate_from_ron_to_redb(db).await?;
    migrate_to_v2(db).await?;
    Ok(())
}

/// State file moved to datastore/private
#[tracing::instrument(err)]
async fn migrate_state_file() -> Result<(), Box<dyn std::error::Error>> {
    let m = PathBuf::from("state.ron");
    if m.try_exists().expect_or_report_with(|| format!("Failed to read {}", m.display())) {
        tracing::warn!("MIGRATING");
        let p = PathBuf::from(super::STATE_FILE);
        tokio::fs::create_dir_all(p.parent().unwrap()).await?;
        tokio::fs::rename(m, &p).await?;
    }
    Ok(())
}

/// Use redb DB instead of Ron state file
#[tracing::instrument(err)]
async fn migrate_from_ron_to_redb(db: &Database) -> Result<(), Box<dyn std::error::Error>> {
    use super::db::db_models::TABLE_VERSION;
    use super::db::db_models::Version;
    use super::db::db_models::v1;

    let ron_state_file_path = PathBuf::from(super::STATE_FILE);

    if ron_state_file_path
        .try_exists()
        .expect_or_report_with(|| format!("Failed to read {}", ron_state_file_path.display()))
    {
        tracing::warn!("MIGRATING");

        let ron_state: PersistentState = {
            let content = tokio::fs::read_to_string(&ron_state_file_path).await?;
            ron::from_str(&content)?
        };

        let write_txn = db.begin_write()?;
        {
            let mut version_table = write_txn.open_table(TABLE_VERSION)?;
            assert!(version_table.is_empty()?);
            version_table.insert((), Version { version: 1 });

            let mut root_table = write_txn.open_table(v1::TABLE_ROOT)?;
            assert!(root_table.is_empty()?);

            let root = v1::Root {
                version: ron_state.version,
                wall: ron_state.wall,
                problems: ron_state.problems,
            };

            root_table.insert((), root)?;
        }
        write_txn.commit()?;

        tracing::info!("Removing ron state");
        tokio::fs::remove_file(ron_state_file_path).await?;
    }

    Ok(())
}

#[tracing::instrument(err)]
async fn migrate_to_v2(db: &Database) -> Result<(), Box<dyn std::error::Error>> {
    use super::db::db_models::TABLE_VERSION;
    use super::db::db_models::Version;
    use super::db::db_models::v1;
    use super::db::db_models::v2;

    let txn = db.begin_write()?;
    {
        let mut version_table = txn.open_table(TABLE_VERSION)?;
        let version = version_table.get(())?.unwrap().value().version;
        if version == 1 {
            tracing::warn!("MIGRATING");
            version_table.insert((), Version { version: 2 })?;

            let root_table_v1 = txn.open_table(v1::TABLE_ROOT)?;
            let root_v1 = root_table_v1.get(())?.unwrap().value();
            txn.delete_table(v1::TABLE_ROOT)?;

            let v1::Root { version: _, wall, problems } = root_v1;

            let mut walls = BTreeSet::new();
            let wall_uid = v2::WallId(uuid::Uuid::new_v4());
            let holds = wall
                .holds
                .into_iter()
                .map(|(hold_position, hold)| {
                    (
                        v2::HoldPosition {
                            row: hold_position.row,
                            col: hold_position.col,
                        },
                        v2::Hold {
                            position: v2::HoldPosition {
                                row: hold.position.row,
                                col: hold.position.col,
                            },
                            image: hold.image.map(|i| v2::Image { filename: i.filename }),
                        },
                    )
                })
                .collect();

            let wall_v2 = v2::Wall {
                uid: wall_uid,
                rows: wall.rows,
                cols: wall.cols,
                holds,
            };

            walls.insert(wall_v2.uid);
            let root_v2 = v2::Root { walls };

            let mut root_table_v2 = txn.open_table(v2::TABLE_ROOT)?;
            root_table_v2.insert((), root_v2)?;

            let mut walls_table = txn.open_table(v2::TABLE_WALLS)?;
            walls_table.insert(wall_v2.uid, wall_v2)?;
        }
    }
    txn.commit()?;

    Ok(())
}
