use crate::web::ApiError;
use lru::LruCache;
use magnetar_model::sea_orm::{EntityTrait, PrimaryKeyTrait};
use magnetar_model::{CalckeyDbError, CalckeyModel};
use std::marker::PhantomData;
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 GenericIdCacheError {
    #[error("Database error: {0}")]
    DbError(#[from] CalckeyDbError),
}

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

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

        api_error
    }
}

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

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

pub struct GenericIdCacheService<E: EntityTrait> {
    cache: Mutex<LruCache<String, CacheEntry<E>>>,
    lifetime_max: Duration,
    db: CalckeyModel,
    _entity_type: PhantomData<E>,
}

impl<E: EntityTrait> GenericIdCacheService<E> {
    pub(super) fn new(db: CalckeyModel, cache_size: usize, entry_lifetime: Duration) -> Self {
        const CACHE_SIZE: usize = 4096;

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

    pub async fn get<'a>(&self, id: &'a str) -> Result<Option<Arc<E::Model>>, GenericIdCacheError>
    where
        <<E as EntityTrait>::PrimaryKey as PrimaryKeyTrait>::ValueType: From<&'a str>,
    {
        let mut read = self.cache.lock().await;
        if let Some(item) = read.peek(id) {
            if item.created + self.lifetime_max >= Instant::now() {
                let data = item.data.clone();
                read.promote(id);
                return Ok(Some(data));
            }
        }
        drop(read);

        let val = E::find_by_id(id)
            .one(self.db.inner())
            .await
            .map_err(CalckeyDbError::from)?;

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

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