use std::{collections::HashSet, sync::Arc};

use crate::storage::visitor_manager::VisitorManager;

#[cfg_attr(test, mockall::automock)]
pub(crate) trait VisitorTrackingRegistry {
    fn add(&mut self, visitor_code: String);
    fn add_all(&mut self, visitor_codes: Vec<String>);
    fn extract(&mut self) -> Vec<String>;
}

const LIMITED_EXTRACTION_THRESHOLD_COEFFICIENT: usize = 2;
const REMOVAL_FACTOR: f64 = 0.8;

pub(crate) struct VisitorTrackingRegistryImpl<V> {
    visitor_manager: Arc<V>,
    storage_limit: usize,
    extraction_limit: usize,
    visitors: HashSet<String>,
}

impl<V: VisitorManager> VisitorTrackingRegistry for VisitorTrackingRegistryImpl<V> {
    fn add(&mut self, visitor_code: String) {
        self.visitors.insert(visitor_code);
    }

    fn add_all(&mut self, visitor_codes: Vec<String>) {
        self.visitors.extend(visitor_codes);
        if self.visitors.len() > self.storage_limit {
            self.erase_nonexistent_visitors();
            self.erase_to_storage_limit();
        }
    }

    fn extract(&mut self) -> Vec<String> {
        if self.should_extract_all_be_used() { self.extract_all() } else { self.extract_limited() }
    }
}

impl<V: VisitorManager> VisitorTrackingRegistryImpl<V> {
    pub fn new(visitor_manager: Arc<V>, storage_limit: usize, extraction_limit: usize) -> Self {
        Self {
            visitor_manager,
            storage_limit,
            extraction_limit,
            visitors: HashSet::new(),
        }
    }

    #[inline]
    fn erase_nonexistent_visitors(&mut self) {
        self.visitors.retain(|vc| self.visitor_manager.get(vc).is_some());
    }

    #[inline]
    fn erase_to_storage_limit(&mut self) {
        let mut left_to_remove = self.visitors.len() as isize - (self.storage_limit as f64 * REMOVAL_FACTOR) as isize;
        if left_to_remove > 0 {
            self.visitors.retain(move |_| {
                if left_to_remove > 0 {
                    left_to_remove -= 1;
                    false
                } else {
                    true
                }
            });
        }
    }

    #[inline]
    fn should_extract_all_be_used(&self) -> bool {
        return self.visitors.len() < self.extraction_limit * LIMITED_EXTRACTION_THRESHOLD_COEFFICIENT;
    }

    #[inline]
    fn extract_all(&mut self) -> Vec<String> {
        let mut extracted = Vec::with_capacity(self.visitors.len());
        extracted.extend(self.visitors.drain());
        return extracted;
    }

    #[inline]
    fn extract_limited(&mut self) -> Vec<String> {
        let mut extracted = Vec::with_capacity(self.extraction_limit);
        let mut left_to_extract = self.extraction_limit;
        extracted.extend(self.visitors.extract_if(move |_| {
            if left_to_extract > 0 {
                left_to_extract -= 1;
                true
            } else {
                false
            }
        }));
        return extracted;
    }
}

#[cfg(test)]
mod tests {
    use crate::{datafile::datafile_manager::MockDataFileManager, storage::visitor_manager::VisitorManagerImpl};

    use super::*;
    use tokio::runtime::Handle;

    const STORAGE_LIMIT: usize = 100;
    const EXTRACTION_LIMIT: usize = 20;
    const LIMITED_EXTRACTION_THRESHOLD: usize = EXTRACTION_LIMIT * LIMITED_EXTRACTION_THRESHOLD_COEFFICIENT;

    fn new_visitor_tracking_registry() -> VisitorTrackingRegistryImpl<VisitorManagerImpl<MockDataFileManager>> {
        let datafile_manager = Arc::new(MockDataFileManager::new());
        let visitor_manager = Arc::new(VisitorManagerImpl::new(datafile_manager, Handle::current()));
        return VisitorTrackingRegistryImpl::new(visitor_manager, STORAGE_LIMIT, EXTRACTION_LIMIT);
    }

    #[tokio::test]
    async fn test_empty_after_initialization() {
        let vtr = new_visitor_tracking_registry();
        assert!(vtr.visitors.is_empty());
    }

    #[tokio::test]
    async fn test_add() {
        let mut vtr = new_visitor_tracking_registry();
        for i in 0..STORAGE_LIMIT * 2 {
            let visitor_code = i.to_string();
            vtr.add(visitor_code.clone());
            assert_eq!(i + 1, vtr.visitors.len());
            assert!(vtr.visitors.contains(&visitor_code));
        }
    }

    #[tokio::test]
    async fn test_add_all_erase_nonexistent_visitors() {
        let mut vtr = new_visitor_tracking_registry();
        for i in 0..STORAGE_LIMIT {
            let visitor_code = i.to_string();
            vtr.add_all(vec![visitor_code.clone()]);
            assert_eq!(i + 1, vtr.visitors.len());
            assert!(vtr.visitors.contains(&visitor_code));
        }
        vtr.add_all(vec!["last".to_owned()]);
        assert!(vtr.visitors.is_empty());
    }

    #[tokio::test]
    async fn test_add_all_erase_to_storage_limit() {
        const EXPECTED_COUNT_AFTER_ERASING: usize = (STORAGE_LIMIT as f64 * REMOVAL_FACTOR) as usize;
        let mut vtr = new_visitor_tracking_registry();
        for i in 0..STORAGE_LIMIT {
            let visitor_code = i.to_string();
            vtr.add_all(vec![visitor_code.clone()]);
            assert_eq!(i + 1, vtr.visitors.len());
            assert!(vtr.visitors.contains(&visitor_code));
            vtr.visitor_manager.get_or_create(&visitor_code);
        }
        vtr.add_all(vec!["last".to_owned()]);
        assert_eq!(EXPECTED_COUNT_AFTER_ERASING, vtr.visitors.len());
        assert!(!vtr.visitors.contains("last"));
    }

    #[tokio::test]
    async fn test_extract_all() {
        // arrange
        const COUNT: usize = LIMITED_EXTRACTION_THRESHOLD_COEFFICIENT - 1;
        let mut vtr = new_visitor_tracking_registry();
        let mut vc_counter = HashSet::new();
        for i in 0..COUNT {
            let visitor_code = i.to_string();
            vc_counter.insert(visitor_code.clone());
            vtr.add(visitor_code);
        }
        assert_eq!(COUNT, vtr.visitors.len());
        // act
        let extracted = vtr.extract();
        // assert
        assert!(vtr.visitors.is_empty());
        for visitor_code in extracted.iter() {
            assert!(vc_counter.remove(visitor_code));
        }
        assert!(vc_counter.is_empty());
    }

    #[tokio::test]
    async fn test_extract_limited() {
        // arrange
        let mut vtr = new_visitor_tracking_registry();
        let mut vc_counter = HashSet::new();
        for i in 0..LIMITED_EXTRACTION_THRESHOLD {
            let visitor_code = i.to_string();
            vc_counter.insert(visitor_code.clone());
            vtr.add(visitor_code);
        }
        assert_eq!(LIMITED_EXTRACTION_THRESHOLD, vtr.visitors.len());
        // act
        let extracted = vtr.extract();
        // assert
        assert_eq!(EXTRACTION_LIMIT * (LIMITED_EXTRACTION_THRESHOLD_COEFFICIENT - 1), vtr.visitors.len());
        assert_eq!(EXTRACTION_LIMIT, extracted.len());
        for visitor_code in vtr.visitors.iter().chain(extracted.iter()) {
            assert!(vc_counter.remove(visitor_code));
        }
        assert!(vc_counter.is_empty());
    }
}
