use crate::web::ApiError;
use lru::LruCache;
use magnetar_common::config::MagnetarConfig;
use magnetar_model::{ck, CalckeyDbError, CalckeyModel};
use std::sync::Arc;
use std::time::{Duration, Instant};
use strum::EnumVariantNames;
use thiserror::Error;
use tokio::sync::Mutex;

#[derive(Debug, Error, EnumVariantNames)]
pub enum RemoteInstanceCacheError {
    #[error("Database error: {0}")]
    DbError(#[from] CalckeyDbError),
}

impl From<RemoteInstanceCacheError> for ApiError {
    fn from(err: RemoteInstanceCacheError) -> Self {
        let mut api_error: ApiError = match err {
            RemoteInstanceCacheError::DbError(err) => err.into(),
        };

        api_error.message = format!("Remote instance cache error: {}", api_error.message);

        api_error
    }
}

#[derive(Debug)]
struct CacheEntry {
    created: Instant,
    data: Arc<ck::instance::Model>,
}

impl CacheEntry {
    fn new(data: Arc<ck::instance::Model>) -> Self {
        Self {
            created: Instant::now(),
            data,
        }
    }
}

pub struct RemoteInstanceCacheService {
    cache: Mutex<LruCache<String, CacheEntry>>,
    lifetime_max: Duration,
    db: CalckeyModel,
    config: &'static MagnetarConfig,
}

impl RemoteInstanceCacheService {
    pub(super) fn new(
        db: CalckeyModel,
        config: &'static MagnetarConfig,
        cache_size: usize,
        entry_lifetime: Duration,
    ) -> Self {
        const CACHE_SIZE: usize = 256;

        Self {
            cache: Mutex::new(LruCache::new(
                cache_size
                    .try_into()
                    .unwrap_or(CACHE_SIZE.try_into().unwrap()),
            )),
            lifetime_max: entry_lifetime,
            db,
            config,
        }
    }

    pub async fn get(
        &self,
        host: &str,
    ) -> Result<Option<Arc<ck::instance::Model>>, RemoteInstanceCacheError> {
        if host == self.config.networking.host {
            return Ok(None);
        }

        let mut read = self.cache.lock().await;
        if let Some(item) = read.peek(host) {
            if item.created + self.lifetime_max >= Instant::now() {
                let data = item.data.clone();
                read.promote(host);
                return Ok(Some(data));
            }
        }
        drop(read);

        let val = self.db.get_instance(host).await?;

        if val.is_none() {
            return Ok(None);
        }

        let mut write = self.cache.lock().await;
        let data = Arc::new(val.unwrap());
        write.put(host.to_string(), CacheEntry::new(data.clone()));
        Ok(Some(data))
    }
}
