use anyhow::Result;
use async_trait::async_trait;
use diesel::prelude::*;

pub use std::sync::Arc;
pub use tokio::sync::Mutex;

use crate::repository::Repository;
use crate::settings::Settings;

use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness};
pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!();

#[async_trait]
pub trait DatabaseAccess {
    async fn with_repository<F, R>(&mut self, f: F) -> R
    where
        F: FnOnce(&mut Repository) -> R + Send,
        R: Send;

    async fn with_settings<F, R>(&mut self, f: F) -> R
    where
        F: FnOnce(&mut Settings) -> R + Send,
        R: Send;
}

pub struct Database {
    pub connection: SqliteConnection,
}

impl Database {
    pub fn new(path: &str) -> Result<Self> {
        let mut connection = SqliteConnection::establish(path)?;

        // Performance optimisations for SQLite. These are safe defaults that speed
        // up concurrent writes and cut the fsync cost dramatically while still
        // keeping durability guarantees that are good enough for an end-user
        // application.
        diesel::sql_query("PRAGMA journal_mode = WAL;").execute(&mut connection)?;
        diesel::sql_query("PRAGMA synchronous = NORMAL;").execute(&mut connection)?;

        connection
            .run_pending_migrations(MIGRATIONS)
            .map_err(|e| anyhow::anyhow!("Error running migrations: {}", e))?;

        Ok(Self { connection })
    }

    pub fn new_in_memory() -> Result<Self> {
        Self::new(":memory:")
    }
}

#[async_trait]
impl DatabaseAccess for Database {
    async fn with_repository<F, R>(&mut self, f: F) -> R
    where
        F: FnOnce(&mut Repository) -> R + Send,
        R: Send,
    {
        let mut repository = Repository::new(&mut self.connection);
        f(&mut repository)
    }

    async fn with_settings<F, R>(&mut self, f: F) -> R
    where
        F: FnOnce(&mut Settings) -> R + Send,
        R: Send,
    {
        let mut settings = Settings::new(&mut self.connection);
        f(&mut settings)
    }
}

#[async_trait]
impl DatabaseAccess for Arc<Mutex<Database>> {
    async fn with_repository<F, R>(&mut self, f: F) -> R
    where
        F: FnOnce(&mut Repository) -> R + Send,
        R: Send,
    {
        let mut database = self.lock().await;
        database.with_repository(f).await
    }

    async fn with_settings<F, R>(&mut self, f: F) -> R
    where
        F: FnOnce(&mut Settings) -> R + Send,
        R: Send,
    {
        let mut database = self.lock().await;
        database.with_settings(f).await
    }
}
