use bincode::Bincode;
use redb::ReadTransaction;
use redb::ReadableTable;
use redb::ReadableTableMetadata;
use redb::TableDefinition;
use redb::WriteTransaction;
use serde::Deserialize;
use serde::Serialize;
use std::collections::BTreeSet;
use std::path::PathBuf;
use std::sync::Arc;

mod bincode;

const DB_FILE: &str = "datastore/private/ascend.redb";

#[derive(Debug, Clone)]
pub struct Database {
    db: Arc<redb::Database>,
}

impl Database {
    #[tracing::instrument(skip_all, err)]
    pub fn create() -> Result<Database, redb::Error> {
        let file = PathBuf::from(DB_FILE);

        // Create parent dirs
        if let Some(parent_dir) = file.parent() {
            std::fs::create_dir_all(parent_dir)?;
        }

        let db = redb::Database::create(file)?;
        Ok(Self { db: Arc::new(db) })
    }

    #[tracing::instrument(skip_all, err)]
    pub async fn read<T>(&self, f: impl FnOnce(&'_ ReadTransaction) -> Result<T, DatabaseOperationError>) -> Result<T, DatabaseOperationError> {
        tokio::task::block_in_place(|| {
            let dbtx = self.db.begin_read()?;
            f(&dbtx)
        })
    }

    #[tracing::instrument(skip_all, err)]
    pub async fn write<T>(&self, f: impl FnOnce(&'_ WriteTransaction) -> Result<T, DatabaseOperationError>) -> Result<T, DatabaseOperationError> {
        tokio::task::block_in_place(|| {
            let dbtx = self.db.begin_write()?;
            let res = f(&dbtx)?;
            dbtx.commit()?;
            Ok(res)
        })
    }

    #[tracing::instrument(skip_all)]
    pub async fn get_version(&self) -> Result<Option<Version>, DatabaseOperationError> {
        self.read(|dbtx| dbtx.open_table(TABLE_VERSION)?.get(()).map(|o| o.map(|v| v.value())).map_err(Into::into))
            .await
    }

    #[tracing::instrument(skip_all)]
    pub async fn set_version(&self, version: Version) -> Result<(), DatabaseOperationError> {
        self.write(|txn| {
            let mut table = txn.open_table(TABLE_VERSION)?;
            table.insert((), version)?;
            Ok(())
        })
        .await
    }
}

#[derive(Debug, derive_more::Error, derive_more::Display, derive_more::From)]
#[display("DB operation error: {_variant}")]
pub enum DatabaseOperationError {
    #[display("redb error")]
    #[from(forward)]
    Redb(#[error(source)] redb::Error),

    #[from(ignore)]
    Custom(#[error(source)] Box<dyn std::error::Error + Send + Sync + 'static>),
}
impl DatabaseOperationError {
    pub fn custom(err: impl std::error::Error + Send + Sync + 'static) -> Self {
        Self::Custom(Box::new(err))
    }
}

pub const TABLE_VERSION: TableDefinition<(), Bincode<Version>> = TableDefinition::new("version");
#[derive(Serialize, Deserialize, Debug, derive_more::Display, PartialEq, Eq, PartialOrd, Ord)]
#[display("{version}")]
pub struct Version {
    pub version: u64,
}
impl Version {
    pub fn current() -> Version {
        Version { version: current::VERSION }
    }
}

// TODO: implement test
#[tracing::instrument(skip_all, err)]
pub async fn init_at_current_version(db: &Database) -> Result<(), DatabaseOperationError> {
    db.write(|txn| {
        let mut version_table = txn.open_table(TABLE_VERSION)?;
        let is_missing_version = version_table.get(())?.is_none();
        if is_missing_version {
            let v = Version::current();
            tracing::warn!("INITIALIZING DATABASE AT VERSION {v}");
            version_table.insert((), v)?;

            // Root table
            {
                let mut table = txn.open_table(current::TABLE_ROOT)?;
                assert!(table.is_empty()?);
                table.insert((), models::Root { walls: BTreeSet::new() })?;
            }

            // Walls table
            {
                // Opening the table creates the table
                let table = txn.open_table(current::TABLE_WALLS)?;
                assert!(table.is_empty()?);
            }

            // User table
            {
                // Opening the table creates the table
                let table = txn.open_table(current::TABLE_USER)?;
                assert!(table.is_empty()?);
            }
        }

        Ok(())
    })
    .await?;

    Ok(())
}

use crate::models;
pub mod current {
    use super::v2;
    use super::v3;
    use super::v4;
    pub use v2::TABLE_ROOT;
    pub use v3::VERSION;
    pub use v4::TABLE_USER;
    pub use v4::TABLE_WALLS;
}

pub mod v4 {
    use crate::models;
    use crate::server::db::bincode::Bincode;
    use redb::TableDefinition;

    pub const VERSION: u64 = 4;

    pub const TABLE_WALLS: TableDefinition<Bincode<models::v2::WallUid>, Bincode<models::v4::Wall>> = TableDefinition::new("walls");
    pub const TABLE_USER: TableDefinition<Bincode<(models::v2::WallUid, models::v4::Problem)>, Bincode<models::v4::UserInteraction>> =
        TableDefinition::new("user");
}

pub mod v3 {
    use crate::models;
    use crate::server::db::bincode::Bincode;
    use redb::TableDefinition;

    pub const VERSION: u64 = 3;

    pub const TABLE_USER: TableDefinition<Bincode<(models::v2::WallUid, models::v2::ProblemUid)>, Bincode<models::v3::UserInteraction>> =
        TableDefinition::new("user");
}

pub mod v2 {
    use crate::models;
    use crate::server::db::bincode::Bincode;
    use redb::TableDefinition;

    pub const VERSION: u64 = 2;

    pub const TABLE_ROOT: TableDefinition<(), Bincode<models::v2::Root>> = TableDefinition::new("root");
    pub const TABLE_WALLS: TableDefinition<Bincode<models::v2::WallUid>, Bincode<models::v2::Wall>> = TableDefinition::new("walls");
    pub const TABLE_PROBLEMS: TableDefinition<Bincode<(models::v2::WallUid, models::v2::ProblemUid)>, Bincode<models::v2::Problem>> =
        TableDefinition::new("problems");
}

pub mod v1 {
    use crate::models;
    use crate::server::db::bincode::Bincode;
    use redb::TableDefinition;

    pub const TABLE_ROOT: TableDefinition<(), Bincode<models::v1::PersistentState>> = TableDefinition::new("root");
}
