pub mod error;

use error::Result;
use log::*;
use model::mutexes::Mutexes;
use sqlx::{types::chrono, MySqlPool};
use std::{fmt::Display, sync::Arc};

pub(crate) trait CollectionMutexe {
    async fn lock(&mut self) -> Result<()>;
    async fn unlock(&mut self) -> Result<()>;
    async fn is_locked(&self) -> Result<bool>;
}

#[derive(Debug, Clone)]
pub struct CollectionMutexeDbImpl {
    db: Arc<MySqlPool>,
    id: Option<String>,
    tenant: String,
    collection: String,
    action: Action,
    duration: std::time::Duration,
}

#[derive(Debug, Clone)]
pub enum Action {
    Create,
    CreateSnapshot,
}

impl Display for Action {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Action::Create => write!(f, "create"),
            Action::CreateSnapshot => write!(f, "create_snapshot"),
        }
    }
}

impl CollectionMutexeDbImpl {
    pub fn new(
        db: Arc<MySqlPool>,
        tenant: String,
        collection: String,
        action: Action,
        duration: std::time::Duration,
    ) -> Self {
        Self {
            db,
            id: None,
            tenant,
            collection,
            action,
            duration,
        }
    }
}

impl CollectionMutexe for CollectionMutexeDbImpl {
    async fn lock(&mut self) -> Result<()> {
        if self.is_locked().await? {
            return Err(error::Error::new("Mutex is already locked".to_string()));
        }

        debug!("Locking mutex: {:?}", self);
        let id = Mutexes::new(
            self.tenant.clone(),
            self.collection.clone(),
            self.action.to_string(),
            self.duration,
        )
        .save(self.db.as_ref())
        .await?;
        self.id = Some(id.clone());
        Ok(())
    }

    async fn unlock(&mut self) -> Result<()> {
        debug!("Unlocking mutex: {:?}", self);
        if let Some(id) = &self.id {
            Mutexes::delete(self.db.as_ref(), id).await?;
            self.id = None;
        }
        Ok(())
    }

    async fn is_locked(&self) -> Result<bool> {
        let mutex = Mutexes::query(
            self.db.as_ref(),
            &self.tenant,
            &self.collection,
            &self.action.to_string(),
        )
        .await?;
        let mutex = match mutex {
            Some(mutex) => mutex,
            None => return Ok(false),
        };

        let expires_at = match mutex.expires_at {
            Some(expires_at) => expires_at,
            None => return Ok(true),
        };

        Ok(expires_at > chrono::Utc::now().naive_utc())
    }
}

impl Drop for CollectionMutexeDbImpl {
    fn drop(&mut self) {
        let id = match self.id {
            Some(ref id) => {
                let id = id.clone();
                self.id = None;
                id
            }
            None => return,
        };
        let db = self.db.clone();
        debug!(
            "Dropping MutexesDBImpl, unlocking mutex: {}, tenant: {}, collection: {}, action: {}",
            id, self.tenant, self.collection, self.action
        );
        tokio::spawn(async move {
            if let Err(e) = Mutexes::delete(db.as_ref(), &id).await {
                error!("Failed to unlock mutex: {}", e);
            }
        });
    }
}
