Skip to main content

scirs2_datasets/
cache.rs

1//! Dataset caching functionality
2
3use crate::error::{DatasetsError, Result};
4use scirs2_core::cache::{CacheBuilder, TTLSizedCache};
5use std::cell::RefCell;
6use std::fs::{self, File};
7use std::hash::{Hash, Hasher};
8use std::io::{Read, Write};
9use std::path::{Path, PathBuf};
10
11/// The base directory name for caching datasets
12const CACHE_DIR_NAME: &str = "scirs2-datasets";
13
14/// Default cache size for in-memory caching
15const DEFAULT_CACHE_SIZE: usize = 100;
16
17/// Default TTL for in-memory cache (in seconds)
18const DEFAULT_CACHE_TTL: u64 = 3600; // 1 hour
19
20/// Default maximum cache size on disk (in bytes) - 500 MB
21const DEFAULT_MAX_CACHE_SIZE: u64 = 500 * 1024 * 1024;
22
23/// Cache directory environment variable
24const CACHE_DIR_ENV: &str = "SCIRS2_CACHE_DIR";
25
26/// Compute SHA256 hash of a file
27#[allow(dead_code)]
28pub fn sha256_hash_file(path: &Path) -> std::result::Result<String, String> {
29    use sha2::{Digest, Sha256};
30
31    let mut file = File::open(path).map_err(|e| format!("Failed to open file: {e}"))?;
32    let mut hasher = Sha256::new();
33    let mut buffer = [0; 8192];
34
35    loop {
36        let bytes_read = file
37            .read(&mut buffer)
38            .map_err(|e| format!("Failed to read file: {e}"))?;
39        if bytes_read == 0 {
40            break;
41        }
42        hasher.update(&buffer[..bytes_read]);
43    }
44
45    Ok(hasher
46        .finalize()
47        .iter()
48        .map(|b| format!("{:02x}", b))
49        .collect())
50}
51
52/// Registry entry for dataset files
53pub struct RegistryEntry {
54    /// SHA256 hash of the file
55    pub sha256: &'static str,
56    /// URL to download the file from
57    pub url: &'static str,
58}
59
60/// Get the platform-specific cache directory for downloading and storing datasets
61///
62/// The cache directory is determined in the following order:
63/// 1. Environment variable `SCIRS2_CACHE_DIR` if set
64/// 2. Platform-specific cache directory:
65///    - Windows: `%LOCALAPPDATA%\scirs2-datasets`
66///    - macOS: `~/Library/Caches/scirs2-datasets`
67///    - Linux/Unix: `~/.cache/scirs2-datasets` (respects XDG_CACHE_HOME)
68/// 3. Fallback to `~/.scirs2-datasets` if platform-specific directory fails
69#[allow(dead_code)]
70pub fn get_cachedir() -> Result<PathBuf> {
71    // Check environment variable first
72    if let Ok(cachedir) = std::env::var(CACHE_DIR_ENV) {
73        let cachepath = PathBuf::from(cachedir);
74        ensuredirectory_exists(&cachepath)?;
75        return Ok(cachepath);
76    }
77
78    // Try platform-specific cache directory
79    if let Some(cachedir) = get_platform_cachedir() {
80        ensuredirectory_exists(&cachedir)?;
81        return Ok(cachedir);
82    }
83
84    // Fallback to home directory
85    let homedir = crate::platform_dirs::home_dir()
86        .ok_or_else(|| DatasetsError::CacheError("Could not find home directory".to_string()))?;
87    let cachedir = homedir.join(format!(".{CACHE_DIR_NAME}"));
88    ensuredirectory_exists(&cachedir)?;
89
90    Ok(cachedir)
91}
92
93/// Get platform-specific cache directory
94#[allow(dead_code)]
95fn get_platform_cachedir() -> Option<PathBuf> {
96    #[cfg(target_os = "windows")]
97    {
98        crate::platform_dirs::data_local_dir().map(|dir| dir.join(CACHE_DIR_NAME))
99    }
100    #[cfg(target_os = "macos")]
101    {
102        crate::platform_dirs::home_dir()
103            .map(|dir| dir.join("Library").join("Caches").join(CACHE_DIR_NAME))
104    }
105    #[cfg(not(any(target_os = "windows", target_os = "macos")))]
106    {
107        // Linux/Unix: Use XDG cache directory
108        if let Ok(xdg_cache) = std::env::var("XDG_CACHE_HOME") {
109            Some(PathBuf::from(xdg_cache).join(CACHE_DIR_NAME))
110        } else {
111            crate::platform_dirs::home_dir().map(|home| home.join(".cache").join(CACHE_DIR_NAME))
112        }
113    }
114}
115
116/// Ensure a directory exists, creating it if necessary
117#[allow(dead_code)]
118fn ensuredirectory_exists(dir: &Path) -> Result<()> {
119    if !dir.exists() {
120        fs::create_dir_all(dir).map_err(|e| {
121            DatasetsError::CacheError(format!("Failed to create cache directory: {e}"))
122        })?;
123    }
124    Ok(())
125}
126
127/// Fetch a dataset file from either cache or download it from the URL
128///
129/// This function will:
130/// 1. Check if the file exists in the cache directory
131/// 2. If not, download it from the URL in the registry entry
132/// 3. Store it in the cache directory
133/// 4. Return the path to the cached file
134///
135/// # Arguments
136///
137/// * `filename` - The name of the file to fetch
138/// * `registry_entry` - Optional registry entry containing URL and SHA256 hash
139///
140/// # Returns
141///
142/// * `Ok(PathBuf)` - Path to the cached file
143/// * `Err(String)` - Error message if fetching fails
144#[cfg(feature = "download-sync")]
145#[allow(dead_code)]
146pub fn fetch_data(
147    filename: &str,
148    registry_entry: Option<&RegistryEntry>,
149) -> std::result::Result<PathBuf, String> {
150    // Get the cache directory
151    let cachedir = match get_cachedir() {
152        Ok(dir) => dir,
153        Err(e) => return Err(format!("Failed to get cache directory: {e}")),
154    };
155
156    // Check if file exists in cache
157    let cachepath = cachedir.join(filename);
158    if cachepath.exists() {
159        return Ok(cachepath);
160    }
161
162    // If not in cache, fetch from the URL
163    let entry = match registry_entry {
164        Some(entry) => entry,
165        None => return Err(format!("No registry entry found for {filename}")),
166    };
167
168    // Create a temporary file to download to
169    let tempdir = tempfile::tempdir().map_err(|e| format!("Failed to create temp dir: {e}"))?;
170    let temp_file = tempdir.path().join(filename);
171
172    // Download the file (ensure a process-default rustls CryptoProvider exists first:
173    // ureq panics on HTTPS connections without one)
174    crate::tls::ensure_default_tls_provider();
175    let response = ureq::get(entry.url)
176        .call()
177        .map_err(|e| format!("Failed to download {filename}: {e}"))?;
178
179    // Read body into memory (ureq 3.x: use into_body which implements Read)
180    let mut body = response.into_body();
181    let bytes = body
182        .read_to_vec()
183        .map_err(|e| format!("Failed to read response body: {e}"))?;
184    let mut file = std::fs::File::create(&temp_file)
185        .map_err(|e| format!("Failed to create temp file: {e}"))?;
186    file.write_all(&bytes)
187        .map_err(|e| format!("Failed to write downloaded file: {e}"))?;
188
189    // Verify the SHA256 hash of the downloaded file if provided
190    if !entry.sha256.is_empty() {
191        let computed_hash = sha256_hash_file(&temp_file)?;
192        if computed_hash != entry.sha256 {
193            return Err(format!(
194                "SHA256 hash mismatch for {filename}: expected {}, got {computed_hash}",
195                entry.sha256
196            ));
197        }
198    }
199
200    // Move the file to the cache
201    fs::create_dir_all(&cachedir).map_err(|e| format!("Failed to create cache dir: {e}"))?;
202    if let Some(parent) = cachepath.parent() {
203        fs::create_dir_all(parent).map_err(|e| format!("Failed to create cache dir: {e}"))?;
204    }
205
206    fs::copy(&temp_file, &cachepath).map_err(|e| format!("Failed to copy to cache: {e}"))?;
207
208    Ok(cachepath)
209}
210
211/// Stub for fetch_data when download-sync feature is disabled
212#[cfg(not(feature = "download-sync"))]
213#[allow(dead_code)]
214pub fn fetch_data(
215    _filename: &str,
216    _registry_entry: Option<&RegistryEntry>,
217) -> std::result::Result<PathBuf, String> {
218    Err("Synchronous download feature is disabled. Enable 'download-sync' feature.".to_string())
219}
220
221/// Cache key for dataset caching with configuration-aware hashing
222#[derive(Clone, Debug, Eq, PartialEq, Hash)]
223pub struct CacheKey {
224    name: String,
225    config_hash: String,
226}
227
228impl CacheKey {
229    /// Create a new cache key from dataset name and configuration
230    pub fn new(name: &str, config: &crate::real_world::RealWorldConfig) -> Self {
231        use std::collections::hash_map::DefaultHasher;
232        use std::hash::{Hash, Hasher};
233
234        let mut hasher = DefaultHasher::new();
235        config.use_cache.hash(&mut hasher);
236        config.download_if_missing.hash(&mut hasher);
237        config.return_preprocessed.hash(&mut hasher);
238        config.subset.hash(&mut hasher);
239        config.random_state.hash(&mut hasher);
240
241        Self {
242            name: name.to_string(),
243            config_hash: format!("{:x}", hasher.finish()),
244        }
245    }
246
247    /// Get the cache key as a string
248    pub fn as_string(&self) -> String {
249        format!("{}_{}", self.name, self.config_hash)
250    }
251}
252
253/// File path wrapper for hashing
254#[derive(Clone, Debug, Eq, PartialEq)]
255struct FileCacheKey(String);
256
257impl Hash for FileCacheKey {
258    fn hash<H: Hasher>(&self, state: &mut H) {
259        self.0.hash(state);
260    }
261}
262
263/// Manages caching of downloaded datasets, using both file-based and in-memory caching
264///
265/// This implementation uses scirs2-core::cache's TTLSizedCache for in-memory caching,
266/// while maintaining the file-based persistence for long-term storage.
267pub struct DatasetCache {
268    /// Directory for file-based caching
269    cachedir: PathBuf,
270    /// In-memory cache for frequently accessed datasets
271    mem_cache: RefCell<TTLSizedCache<FileCacheKey, Vec<u8>>>,
272    /// Maximum cache size in bytes (0 means unlimited)
273    max_cache_size: u64,
274    /// Whether to operate in offline mode (no downloads)
275    offline_mode: bool,
276}
277
278impl Default for DatasetCache {
279    fn default() -> Self {
280        let cachedir = get_cachedir().expect("Could not get cache directory");
281
282        let mem_cache = RefCell::new(
283            CacheBuilder::new()
284                .with_size(DEFAULT_CACHE_SIZE)
285                .with_ttl(DEFAULT_CACHE_TTL)
286                .build_sized_cache(),
287        );
288
289        // Check if offline mode is enabled via environment variable
290        let offline_mode = std::env::var("SCIRS2_OFFLINE")
291            .map(|v| v.to_lowercase() == "true" || v == "1")
292            .unwrap_or(false);
293
294        DatasetCache {
295            cachedir,
296            mem_cache,
297            max_cache_size: DEFAULT_MAX_CACHE_SIZE,
298            offline_mode,
299        }
300    }
301}
302
303impl DatasetCache {
304    /// Create a new dataset cache with the given cache directory and default memory cache
305    pub fn new(cachedir: PathBuf) -> Self {
306        let mem_cache = RefCell::new(
307            CacheBuilder::new()
308                .with_size(DEFAULT_CACHE_SIZE)
309                .with_ttl(DEFAULT_CACHE_TTL)
310                .build_sized_cache(),
311        );
312
313        let offline_mode = std::env::var("SCIRS2_OFFLINE")
314            .map(|v| v.to_lowercase() == "true" || v == "1")
315            .unwrap_or(false);
316
317        DatasetCache {
318            cachedir,
319            mem_cache,
320            max_cache_size: DEFAULT_MAX_CACHE_SIZE,
321            offline_mode,
322        }
323    }
324
325    /// Create a new dataset cache with custom settings
326    pub fn with_config(cachedir: PathBuf, cache_size: usize, ttl_seconds: u64) -> Self {
327        let mem_cache = RefCell::new(
328            CacheBuilder::new()
329                .with_size(cache_size)
330                .with_ttl(ttl_seconds)
331                .build_sized_cache(),
332        );
333
334        let offline_mode = std::env::var("SCIRS2_OFFLINE")
335            .map(|v| v.to_lowercase() == "true" || v == "1")
336            .unwrap_or(false);
337
338        DatasetCache {
339            cachedir,
340            mem_cache,
341            max_cache_size: DEFAULT_MAX_CACHE_SIZE,
342            offline_mode,
343        }
344    }
345
346    /// Create a new dataset cache with comprehensive configuration
347    pub fn with_full_config(
348        cachedir: PathBuf,
349        cache_size: usize,
350        ttl_seconds: u64,
351        max_cache_size: u64,
352        offline_mode: bool,
353    ) -> Self {
354        let mem_cache = RefCell::new(
355            CacheBuilder::new()
356                .with_size(cache_size)
357                .with_ttl(ttl_seconds)
358                .build_sized_cache(),
359        );
360
361        DatasetCache {
362            cachedir,
363            mem_cache,
364            max_cache_size,
365            offline_mode,
366        }
367    }
368
369    /// Create the cache directory if it doesn't exist
370    pub fn ensure_cachedir(&self) -> Result<()> {
371        if !self.cachedir.exists() {
372            fs::create_dir_all(&self.cachedir).map_err(|e| {
373                DatasetsError::CacheError(format!("Failed to create cache directory: {e}"))
374            })?;
375        }
376        Ok(())
377    }
378
379    /// Get the path to a cached file
380    pub fn get_cachedpath(&self, name: &str) -> PathBuf {
381        self.cachedir.join(name)
382    }
383
384    /// Check if a file is already cached (either in memory or on disk)
385    pub fn is_cached(&self, name: &str) -> bool {
386        // Check memory cache first
387        let key = FileCacheKey(name.to_string());
388        if self.mem_cache.borrow_mut().get(&key).is_some() {
389            return true;
390        }
391
392        // Then check file system
393        self.get_cachedpath(name).exists()
394    }
395
396    /// Read a cached file as bytes
397    ///
398    /// This method checks the in-memory cache first, and falls back to the file system if needed.
399    /// When reading from the file system, the result is also stored in the in-memory cache.
400    pub fn read_cached(&self, name: &str) -> Result<Vec<u8>> {
401        // Try memory cache first
402        let key = FileCacheKey(name.to_string());
403        if let Some(data) = self.mem_cache.borrow_mut().get(&key) {
404            return Ok(data);
405        }
406
407        // Fall back to file system cache
408        let path = self.get_cachedpath(name);
409        if !path.exists() {
410            return Err(DatasetsError::CacheError(format!(
411                "Cached file does not exist: {name}"
412            )));
413        }
414
415        let mut file = File::open(path)
416            .map_err(|e| DatasetsError::CacheError(format!("Failed to open cached file: {e}")))?;
417
418        let mut buffer = Vec::new();
419        file.read_to_end(&mut buffer)
420            .map_err(|e| DatasetsError::CacheError(format!("Failed to read cached file: {e}")))?;
421
422        // Update memory cache
423        self.mem_cache.borrow_mut().insert(key, buffer.clone());
424
425        Ok(buffer)
426    }
427
428    /// Write data to both the file cache and memory cache
429    pub fn write_cached(&self, name: &str, data: &[u8]) -> Result<()> {
430        self.ensure_cachedir()?;
431
432        // Check if writing this file would exceed cache size limit
433        if self.max_cache_size > 0 {
434            let current_size = self.get_cache_size_bytes()?;
435            let new_file_size = data.len() as u64;
436
437            if current_size + new_file_size > self.max_cache_size {
438                self.cleanup_cache_to_fit(new_file_size)?;
439            }
440        }
441
442        // Write to file system cache
443        let path = self.get_cachedpath(name);
444        let mut file = File::create(path)
445            .map_err(|e| DatasetsError::CacheError(format!("Failed to create cache file: {e}")))?;
446
447        file.write_all(data).map_err(|e| {
448            DatasetsError::CacheError(format!("Failed to write to cache file: {e}"))
449        })?;
450
451        // Update memory cache
452        let key = FileCacheKey(name.to_string());
453        self.mem_cache.borrow_mut().insert(key, data.to_vec());
454
455        Ok(())
456    }
457
458    /// Clear the entire cache (both memory and file-based)
459    pub fn clear_cache(&self) -> Result<()> {
460        // Clear file system cache
461        if self.cachedir.exists() {
462            fs::remove_dir_all(&self.cachedir)
463                .map_err(|e| DatasetsError::CacheError(format!("Failed to clear cache: {e}")))?;
464        }
465
466        // Clear memory cache
467        self.mem_cache.borrow_mut().clear();
468
469        Ok(())
470    }
471
472    /// Remove a specific cached file (from both memory and file system)
473    pub fn remove_cached(&self, name: &str) -> Result<()> {
474        // Remove from file system
475        let path = self.get_cachedpath(name);
476        if path.exists() {
477            fs::remove_file(path).map_err(|e| {
478                DatasetsError::CacheError(format!("Failed to remove cached file: {e}"))
479            })?;
480        }
481
482        // Remove from memory cache
483        let key = FileCacheKey(name.to_string());
484        self.mem_cache.borrow_mut().remove(&key);
485
486        Ok(())
487    }
488
489    /// Compute a hash for a filename or URL
490    pub fn hash_filename(name: &str) -> String {
491        let hash = blake3::hash(name.as_bytes());
492        hash.to_hex().to_string()
493    }
494
495    /// Get the total size of the cache in bytes
496    pub fn get_cache_size_bytes(&self) -> Result<u64> {
497        let mut total_size = 0u64;
498
499        if self.cachedir.exists() {
500            let entries = fs::read_dir(&self.cachedir).map_err(|e| {
501                DatasetsError::CacheError(format!("Failed to read cache directory: {e}"))
502            })?;
503
504            for entry in entries {
505                let entry = entry.map_err(|e| {
506                    DatasetsError::CacheError(format!("Failed to read directory entry: {e}"))
507                })?;
508
509                if let Ok(metadata) = entry.metadata() {
510                    if metadata.is_file() {
511                        total_size += metadata.len();
512                    }
513                }
514            }
515        }
516
517        Ok(total_size)
518    }
519
520    /// Clean up cache to fit a new file of specified size
521    ///
522    /// This method removes the oldest files first until there's enough space
523    /// for the new file plus some buffer space.
524    fn cleanup_cache_to_fit(&self, needed_size: u64) -> Result<()> {
525        if self.max_cache_size == 0 {
526            return Ok(()); // No _size limit
527        }
528
529        let current_size = self.get_cache_size_bytes()?;
530        let target_size = (self.max_cache_size as f64 * 0.8) as u64; // Leave 20% buffer
531        let total_needed = current_size + needed_size;
532
533        if total_needed <= target_size {
534            return Ok(()); // No cleanup needed
535        }
536
537        let size_to_free = total_needed - target_size;
538
539        // Get all files with their modification times
540        let mut files_with_times = Vec::new();
541
542        if self.cachedir.exists() {
543            let entries = fs::read_dir(&self.cachedir).map_err(|e| {
544                DatasetsError::CacheError(format!("Failed to read cache directory: {e}"))
545            })?;
546
547            for entry in entries {
548                let entry = entry.map_err(|e| {
549                    DatasetsError::CacheError(format!("Failed to read directory entry: {e}"))
550                })?;
551
552                if let Ok(metadata) = entry.metadata() {
553                    if metadata.is_file() {
554                        if let Ok(modified) = metadata.modified() {
555                            files_with_times.push((entry.path(), metadata.len(), modified));
556                        }
557                    }
558                }
559            }
560        }
561
562        // Sort by modification time (oldest first)
563        files_with_times.sort_by_key(|(_path, _size, modified)| *modified);
564
565        // Remove files until we've freed enough space
566        let mut freed_size = 0u64;
567        for (path, size, _modified) in files_with_times {
568            if freed_size >= size_to_free {
569                break;
570            }
571
572            // Remove from memory cache first
573            if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
574                let key = FileCacheKey(filename.to_string());
575                self.mem_cache.borrow_mut().remove(&key);
576            }
577
578            // Remove file
579            if let Err(e) = fs::remove_file(&path) {
580                eprintln!("Warning: Failed to remove cache file {path:?}: {e}");
581            } else {
582                freed_size += size;
583            }
584        }
585
586        Ok(())
587    }
588
589    /// Set offline mode
590    pub fn set_offline_mode(&mut self, offline: bool) {
591        self.offline_mode = offline;
592    }
593
594    /// Check if cache is in offline mode
595    pub fn is_offline(&self) -> bool {
596        self.offline_mode
597    }
598
599    /// Set maximum cache size in bytes (0 for unlimited)
600    pub fn set_max_cache_size(&mut self, max_size: u64) {
601        self.max_cache_size = max_size;
602    }
603
604    /// Get maximum cache size in bytes
605    pub fn max_cache_size(&self) -> u64 {
606        self.max_cache_size
607    }
608
609    /// Put data into the cache (alias for write_cached)
610    pub fn put(&self, name: &str, data: &[u8]) -> Result<()> {
611        self.write_cached(name, data)
612    }
613
614    /// Get detailed cache information
615    pub fn get_detailed_stats(&self) -> Result<DetailedCacheStats> {
616        let mut total_size = 0u64;
617        let mut file_count = 0usize;
618        let mut files = Vec::new();
619
620        if self.cachedir.exists() {
621            let entries = fs::read_dir(&self.cachedir).map_err(|e| {
622                DatasetsError::CacheError(format!("Failed to read cache directory: {e}"))
623            })?;
624
625            for entry in entries {
626                let entry = entry.map_err(|e| {
627                    DatasetsError::CacheError(format!("Failed to read directory entry: {e}"))
628                })?;
629
630                if let Ok(metadata) = entry.metadata() {
631                    if metadata.is_file() {
632                        let size = metadata.len();
633                        total_size += size;
634                        file_count += 1;
635
636                        if let Some(filename) = entry.file_name().to_str() {
637                            files.push(CacheFileInfo {
638                                name: filename.to_string(),
639                                size_bytes: size,
640                                modified: metadata.modified().ok(),
641                            });
642                        }
643                    }
644                }
645            }
646        }
647
648        // Sort files by size (largest first)
649        files.sort_by_key(|f| std::cmp::Reverse(f.size_bytes));
650
651        Ok(DetailedCacheStats {
652            total_size_bytes: total_size,
653            file_count,
654            cachedir: self.cachedir.clone(),
655            max_cache_size: self.max_cache_size,
656            offline_mode: self.offline_mode,
657            files,
658        })
659    }
660}
661
662/// Downloads data from a URL and returns it as bytes, using the cache when possible
663#[cfg(feature = "download")]
664#[allow(dead_code)]
665pub fn download_data(_url: &str, force_download: bool) -> Result<Vec<u8>> {
666    let cache = DatasetCache::default();
667    let cache_key = DatasetCache::hash_filename(_url);
668
669    // Check if the data is already cached
670    if !force_download && cache.is_cached(&cache_key) {
671        return cache.read_cached(&cache_key);
672    }
673
674    // Download the data (ensure a process-default rustls CryptoProvider exists first:
675    // `reqwest::blocking::get` builds a Client eagerly, which panics without one)
676    crate::tls::ensure_default_tls_provider();
677    let response = reqwest::blocking::get(_url).map_err(|e| {
678        DatasetsError::DownloadError(format!("Failed to download from {_url}: {e}"))
679    })?;
680
681    if !response.status().is_success() {
682        return Err(DatasetsError::DownloadError(format!(
683            "Failed to download from {_url}: HTTP status {}",
684            response.status()
685        )));
686    }
687
688    let data = response
689        .bytes()
690        .map_err(|e| DatasetsError::DownloadError(format!("Failed to read response data: {e}")))?;
691
692    let data_vec = data.to_vec();
693
694    // Cache the data
695    cache.write_cached(&cache_key, &data_vec)?;
696
697    Ok(data_vec)
698}
699
700// Stub for when download feature is not enabled
701#[cfg(not(feature = "download"))]
702/// Downloads data from a URL or retrieves it from cache
703///
704/// This is a stub implementation when the download feature is not enabled.
705/// It returns an error informing the user to enable the download feature.
706///
707/// # Arguments
708///
709/// * `_url` - The URL to download from
710/// * `_force_download` - If true, force a new download instead of using cache
711///
712/// # Returns
713///
714/// * An error indicating that the download feature is not enabled
715#[allow(dead_code)]
716pub fn download_data(_url: &str, _force_download: bool) -> Result<Vec<u8>> {
717    Err(DatasetsError::Other(
718        "Download feature is not enabled. Recompile with --features download".to_string(),
719    ))
720}
721
722/// Cache management utilities
723pub struct CacheManager {
724    cache: DatasetCache,
725}
726
727impl CacheManager {
728    /// Create a new cache manager with default settings
729    pub fn new() -> Result<Self> {
730        let cachedir = get_cachedir()?;
731        Ok(Self {
732            cache: DatasetCache::with_config(cachedir, DEFAULT_CACHE_SIZE, DEFAULT_CACHE_TTL),
733        })
734    }
735
736    /// Create a new cache manager with custom settings
737    pub fn with_config(cachedir: PathBuf, cache_size: usize, ttl_seconds: u64) -> Self {
738        Self {
739            cache: DatasetCache::with_config(cachedir, cache_size, ttl_seconds),
740        }
741    }
742
743    /// Get a dataset from cache using CacheKey
744    pub fn get(&self, key: &CacheKey) -> Result<Option<crate::utils::Dataset>> {
745        let name = key.as_string();
746        if self.cache.is_cached(&name) {
747            match self.cache.read_cached(&name) {
748                Ok(cached_data) => {
749                    match serde_json::from_slice::<crate::utils::Dataset>(&cached_data) {
750                        Ok(dataset) => Ok(Some(dataset)),
751                        Err(e) => {
752                            // If deserialization fails, consider the cache entry invalid
753                            self.cache
754                                .mem_cache
755                                .borrow_mut()
756                                .remove(&FileCacheKey(name.clone()));
757                            Err(DatasetsError::CacheError(format!(
758                                "Failed to deserialize cached dataset: {e}"
759                            )))
760                        }
761                    }
762                }
763                Err(e) => Err(DatasetsError::CacheError(format!(
764                    "Failed to read cached data: {e}"
765                ))),
766            }
767        } else {
768            Ok(None)
769        }
770    }
771
772    /// Put a dataset into cache using CacheKey
773    pub fn put(&self, key: &CacheKey, dataset: &crate::utils::Dataset) -> Result<()> {
774        let name = key.as_string();
775
776        // Serialize the dataset to JSON bytes for caching
777        let serialized = serde_json::to_vec(dataset)
778            .map_err(|e| DatasetsError::CacheError(format!("Failed to serialize dataset: {e}")))?;
779
780        // Write the serialized data to cache
781        self.cache
782            .write_cached(&name, &serialized)
783            .map_err(|e| DatasetsError::CacheError(format!("Failed to write to cache: {e}")))
784    }
785
786    /// Create a cache manager with comprehensive configuration
787    pub fn with_full_config(
788        cachedir: PathBuf,
789        cache_size: usize,
790        ttl_seconds: u64,
791        max_cache_size: u64,
792        offline_mode: bool,
793    ) -> Self {
794        Self {
795            cache: DatasetCache::with_full_config(
796                cachedir,
797                cache_size,
798                ttl_seconds,
799                max_cache_size,
800                offline_mode,
801            ),
802        }
803    }
804
805    /// Get basic cache statistics
806    pub fn get_stats(&self) -> CacheStats {
807        let cachedir = &self.cache.cachedir;
808        let mut total_size = 0u64;
809        let mut file_count = 0usize;
810
811        if cachedir.exists() {
812            if let Ok(entries) = fs::read_dir(cachedir) {
813                for entry in entries.flatten() {
814                    if let Ok(metadata) = entry.metadata() {
815                        if metadata.is_file() {
816                            total_size += metadata.len();
817                            file_count += 1;
818                        }
819                    }
820                }
821            }
822        }
823
824        CacheStats {
825            total_size_bytes: total_size,
826            file_count,
827            cachedir: cachedir.clone(),
828        }
829    }
830
831    /// Get detailed cache statistics
832    pub fn get_detailed_stats(&self) -> Result<DetailedCacheStats> {
833        self.cache.get_detailed_stats()
834    }
835
836    /// Set offline mode
837    pub fn set_offline_mode(&mut self, offline: bool) {
838        self.cache.set_offline_mode(offline);
839    }
840
841    /// Check if in offline mode
842    pub fn is_offline(&self) -> bool {
843        self.cache.is_offline()
844    }
845
846    /// Set maximum cache size in bytes (0 for unlimited)
847    pub fn set_max_cache_size(&mut self, max_size: u64) {
848        self.cache.set_max_cache_size(max_size);
849    }
850
851    /// Get maximum cache size in bytes
852    pub fn max_cache_size(&self) -> u64 {
853        self.cache.max_cache_size()
854    }
855
856    /// Clear all cached data
857    pub fn clear_all(&self) -> Result<()> {
858        self.cache.clear_cache()
859    }
860
861    /// Remove specific cached file
862    pub fn remove(&self, name: &str) -> Result<()> {
863        self.cache.remove_cached(name)
864    }
865
866    /// Remove old files to free up space
867    pub fn cleanup_old_files(&self, target_size: u64) -> Result<()> {
868        self.cache.cleanup_cache_to_fit(target_size)
869    }
870
871    /// List all cached files
872    pub fn list_cached_files(&self) -> Result<Vec<String>> {
873        let cachedir = &self.cache.cachedir;
874        let mut files = Vec::new();
875
876        if cachedir.exists() {
877            let entries = fs::read_dir(cachedir).map_err(|e| {
878                DatasetsError::CacheError(format!("Failed to read cache directory: {e}"))
879            })?;
880
881            for entry in entries {
882                let entry = entry.map_err(|e| {
883                    DatasetsError::CacheError(format!("Failed to read directory entry: {e}"))
884                })?;
885
886                if let Some(filename) = entry.file_name().to_str() {
887                    files.push(filename.to_string());
888                }
889            }
890        }
891
892        files.sort();
893        Ok(files)
894    }
895
896    /// Get cache directory path
897    pub fn cachedir(&self) -> &PathBuf {
898        &self.cache.cachedir
899    }
900
901    /// Check if a file is cached
902    pub fn is_cached(&self, name: &str) -> bool {
903        self.cache.is_cached(name)
904    }
905
906    /// Print detailed cache report
907    pub fn print_cache_report(&self) -> Result<()> {
908        let stats = self.get_detailed_stats()?;
909
910        println!("=== Cache Report ===");
911        println!("Cache Directory: {}", stats.cachedir.display());
912        println!(
913            "Total Size: {} ({} files)",
914            stats.formatted_size(),
915            stats.file_count
916        );
917        println!("Max Size: {}", stats.formatted_max_size());
918
919        if stats.max_cache_size > 0 {
920            println!("Usage: {:.1}%", stats.usage_percentage() * 100.0);
921        }
922
923        println!(
924            "Offline Mode: {}",
925            if stats.offline_mode {
926                "Enabled"
927            } else {
928                "Disabled"
929            }
930        );
931
932        if !stats.files.is_empty() {
933            println!("\nCached Files:");
934            for file in &stats.files {
935                println!(
936                    "  {} - {} ({})",
937                    file.name,
938                    file.formatted_size(),
939                    file.formatted_modified()
940                );
941            }
942        }
943
944        Ok(())
945    }
946}
947
948/// Cache statistics
949pub struct CacheStats {
950    /// Total size of all cached files in bytes
951    pub total_size_bytes: u64,
952    /// Number of cached files
953    pub file_count: usize,
954    /// Cache directory path
955    pub cachedir: PathBuf,
956}
957
958/// Detailed cache statistics with file-level information
959pub struct DetailedCacheStats {
960    /// Total size of all cached files in bytes
961    pub total_size_bytes: u64,
962    /// Number of cached files
963    pub file_count: usize,
964    /// Cache directory path
965    pub cachedir: PathBuf,
966    /// Maximum cache size (0 = unlimited)
967    pub max_cache_size: u64,
968    /// Whether cache is in offline mode
969    pub offline_mode: bool,
970    /// Information about individual cached files
971    pub files: Vec<CacheFileInfo>,
972}
973
974/// Information about a cached file
975#[derive(Debug, Clone)]
976pub struct CacheFileInfo {
977    /// Name of the cached file
978    pub name: String,
979    /// Size in bytes
980    pub size_bytes: u64,
981    /// Last modified time
982    pub modified: Option<std::time::SystemTime>,
983}
984
985impl CacheStats {
986    /// Get total size formatted as human-readable string
987    pub fn formatted_size(&self) -> String {
988        format_bytes(self.total_size_bytes)
989    }
990}
991
992impl DetailedCacheStats {
993    /// Get total size formatted as human-readable string
994    pub fn formatted_size(&self) -> String {
995        format_bytes(self.total_size_bytes)
996    }
997
998    /// Get max cache size formatted as human-readable string
999    pub fn formatted_max_size(&self) -> String {
1000        if self.max_cache_size == 0 {
1001            "Unlimited".to_string()
1002        } else {
1003            format_bytes(self.max_cache_size)
1004        }
1005    }
1006
1007    /// Get cache usage percentage (0.0-1.0)
1008    pub fn usage_percentage(&self) -> f64 {
1009        if self.max_cache_size == 0 {
1010            0.0
1011        } else {
1012            self.total_size_bytes as f64 / self.max_cache_size as f64
1013        }
1014    }
1015}
1016
1017impl CacheFileInfo {
1018    /// Get file size formatted as human-readable string
1019    pub fn formatted_size(&self) -> String {
1020        format_bytes(self.size_bytes)
1021    }
1022
1023    /// Get formatted modification time
1024    pub fn formatted_modified(&self) -> String {
1025        match &self.modified {
1026            Some(time) => {
1027                if let Ok(now) = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)
1028                {
1029                    if let Ok(modified) = time.duration_since(std::time::UNIX_EPOCH) {
1030                        let diff_secs = now.as_secs().saturating_sub(modified.as_secs());
1031                        let days = diff_secs / 86400;
1032                        let hours = (diff_secs % 86400) / 3600;
1033                        let mins = (diff_secs % 3600) / 60;
1034
1035                        if days > 0 {
1036                            format!("{days} days ago")
1037                        } else if hours > 0 {
1038                            format!("{hours} hours ago")
1039                        } else if mins > 0 {
1040                            format!("{mins} minutes ago")
1041                        } else {
1042                            "Just now".to_string()
1043                        }
1044                    } else {
1045                        "Unknown".to_string()
1046                    }
1047                } else {
1048                    "Unknown".to_string()
1049                }
1050            }
1051            None => "Unknown".to_string(),
1052        }
1053    }
1054}
1055
1056/// Format bytes as human-readable string
1057#[allow(dead_code)]
1058fn format_bytes(bytes: u64) -> String {
1059    let size = bytes as f64;
1060    if size < 1024.0 {
1061        format!("{size} B")
1062    } else if size < 1024.0 * 1024.0 {
1063        format!("{:.1} KB", size / 1024.0)
1064    } else if size < 1024.0 * 1024.0 * 1024.0 {
1065        format!("{:.1} MB", size / (1024.0 * 1024.0))
1066    } else {
1067        format!("{:.1} GB", size / (1024.0 * 1024.0 * 1024.0))
1068    }
1069}
1070
1071/// Batch operation result containing success/failure information
1072#[derive(Debug, Clone)]
1073pub struct BatchResult {
1074    /// Number of successful operations
1075    pub success_count: usize,
1076    /// Number of failed operations
1077    pub failure_count: usize,
1078    /// List of failed items with error messages
1079    pub failures: Vec<(String, String)>,
1080    /// Total bytes processed
1081    pub total_bytes: u64,
1082    /// Total time taken for the batch operation
1083    pub elapsed_time: std::time::Duration,
1084}
1085
1086impl BatchResult {
1087    /// Create a new empty batch result
1088    pub fn new() -> Self {
1089        Self {
1090            success_count: 0,
1091            failure_count: 0,
1092            failures: Vec::new(),
1093            total_bytes: 0,
1094            elapsed_time: std::time::Duration::ZERO,
1095        }
1096    }
1097
1098    /// Check if all operations were successful
1099    pub fn is_all_success(&self) -> bool {
1100        self.failure_count == 0
1101    }
1102
1103    /// Get success rate as percentage
1104    pub fn success_rate(&self) -> f64 {
1105        let total = self.success_count + self.failure_count;
1106        if total == 0 {
1107            0.0
1108        } else {
1109            (self.success_count as f64 / total as f64) * 100.0
1110        }
1111    }
1112
1113    /// Get formatted summary
1114    pub fn summary(&self) -> String {
1115        format!(
1116            "Batch completed: {}/{} successful ({:.1}%), {} bytes processed in {:.2}s",
1117            self.success_count,
1118            self.success_count + self.failure_count,
1119            self.success_rate(),
1120            format_bytes(self.total_bytes),
1121            self.elapsed_time.as_secs_f64()
1122        )
1123    }
1124}
1125
1126impl Default for BatchResult {
1127    fn default() -> Self {
1128        Self::new()
1129    }
1130}
1131
1132/// Batch operations manager for dataset caching
1133pub struct BatchOperations {
1134    cache: CacheManager,
1135    parallel: bool,
1136    max_retries: usize,
1137    retry_delay: std::time::Duration,
1138}
1139
1140impl BatchOperations {
1141    /// Create a new batch operations manager
1142    pub fn new(cache: CacheManager) -> Self {
1143        Self {
1144            cache,
1145            parallel: true,
1146            max_retries: 3,
1147            retry_delay: std::time::Duration::from_millis(1000),
1148        }
1149    }
1150
1151    /// Configure parallel processing
1152    pub fn with_parallel(mut self, parallel: bool) -> Self {
1153        self.parallel = parallel;
1154        self
1155    }
1156
1157    /// Configure retry settings
1158    pub fn with_retry_config(
1159        mut self,
1160        max_retries: usize,
1161        retry_delay: std::time::Duration,
1162    ) -> Self {
1163        self.max_retries = max_retries;
1164        self.retry_delay = retry_delay;
1165        self
1166    }
1167
1168    /// Download multiple datasets in batch
1169    #[cfg(feature = "download")]
1170    pub fn batch_download(&self, urls_andnames: &[(&str, &str)]) -> BatchResult {
1171        let start_time = std::time::Instant::now();
1172        let mut result = BatchResult::new();
1173
1174        if self.parallel {
1175            self.batch_download_parallel(urls_andnames, &mut result)
1176        } else {
1177            self.batch_download_sequential(urls_andnames, &mut result)
1178        }
1179
1180        result.elapsed_time = start_time.elapsed();
1181        result
1182    }
1183
1184    #[cfg(feature = "download")]
1185    fn batch_download_parallel(&self, urls_andnames: &[(&str, &str)], result: &mut BatchResult) {
1186        use std::fs::File;
1187        use std::io::Write;
1188        use std::sync::{Arc, Mutex};
1189        use std::thread;
1190
1191        // Ensure cache directory exists before spawning threads
1192        if let Err(e) = self.cache.cache.ensure_cachedir() {
1193            result.failure_count += urls_andnames.len();
1194            for &(_, name) in urls_andnames {
1195                result
1196                    .failures
1197                    .push((name.to_string(), format!("Cache setup failed: {e}")));
1198            }
1199            return;
1200        }
1201
1202        let result_arc = Arc::new(Mutex::new(BatchResult::new()));
1203        let cachedir = self.cache.cache.cachedir.clone();
1204        let max_retries = self.max_retries;
1205        let retry_delay = self.retry_delay;
1206
1207        let handles: Vec<_> = urls_andnames
1208            .iter()
1209            .map(|&(url, name)| {
1210                let result_clone = Arc::clone(&result_arc);
1211                let url = url.to_string();
1212                let name = name.to_string();
1213                let cachedir = cachedir.clone();
1214
1215                thread::spawn(move || {
1216                    let mut success = false;
1217                    let mut last_error = String::new();
1218                    let mut downloaded_data = Vec::new();
1219
1220                    for attempt in 0..=max_retries {
1221                        match download_data(&url, false) {
1222                            Ok(data) => {
1223                                // Write directly to filesystem (bypassing RefCell memory cache)
1224                                let path = cachedir.join(&name);
1225                                match File::create(&path) {
1226                                    Ok(mut file) => match file.write_all(&data) {
1227                                        Ok(_) => {
1228                                            let mut r =
1229                                                result_clone.lock().expect("Operation failed");
1230                                            r.success_count += 1;
1231                                            r.total_bytes += data.len() as u64;
1232                                            downloaded_data = data;
1233                                            success = true;
1234                                            break;
1235                                        }
1236                                        Err(e) => {
1237                                            last_error = format!("Failed to write cache file: {e}");
1238                                        }
1239                                    },
1240                                    Err(e) => {
1241                                        last_error = format!("Failed to create cache file: {e}");
1242                                    }
1243                                }
1244                            }
1245                            Err(e) => {
1246                                last_error = format!("Download failed: {e}");
1247                                if attempt < max_retries {
1248                                    thread::sleep(retry_delay);
1249                                }
1250                            }
1251                        }
1252                    }
1253
1254                    if !success {
1255                        let mut r = result_clone.lock().expect("Operation failed");
1256                        r.failure_count += 1;
1257                        r.failures.push((name.clone(), last_error));
1258                    }
1259
1260                    (name, success, downloaded_data)
1261                })
1262            })
1263            .collect();
1264
1265        // Collect results and update memory cache for successful downloads
1266        let mut successful_downloads = Vec::new();
1267        for handle in handles {
1268            if let Ok((name, success, data)) = handle.join() {
1269                if success && !data.is_empty() {
1270                    successful_downloads.push((name, data));
1271                }
1272            }
1273        }
1274
1275        // Merge the results from the arc back into the original result
1276        if let Ok(arc_result) = result_arc.lock() {
1277            result.success_count += arc_result.success_count;
1278            result.failure_count += arc_result.failure_count;
1279            result.failures.extend(arc_result.failures.clone());
1280        }
1281
1282        // Update memory cache after all threads complete
1283        for (name, data) in successful_downloads {
1284            let key = FileCacheKey(name);
1285            self.cache.cache.mem_cache.borrow_mut().insert(key, data);
1286        }
1287    }
1288
1289    #[cfg(feature = "download")]
1290    fn batch_download_sequential(&self, urls_andnames: &[(&str, &str)], result: &mut BatchResult) {
1291        for &(url, name) in urls_andnames {
1292            let mut success = false;
1293            let mut last_error = String::new();
1294
1295            for attempt in 0..=self.max_retries {
1296                match download_data(url, false) {
1297                    Ok(data) => match self.cache.cache.write_cached(name, &data) {
1298                        Ok(_) => {
1299                            result.success_count += 1;
1300                            result.total_bytes += data.len() as u64;
1301                            success = true;
1302                            break;
1303                        }
1304                        Err(e) => {
1305                            last_error = format!("Cache write failed: {e}");
1306                        }
1307                    },
1308                    Err(e) => {
1309                        last_error = format!("Download failed: {e}");
1310                        if attempt < self.max_retries {
1311                            std::thread::sleep(self.retry_delay);
1312                        }
1313                    }
1314                }
1315            }
1316
1317            if !success {
1318                result.failure_count += 1;
1319                result.failures.push((name.to_string(), last_error));
1320            }
1321        }
1322    }
1323
1324    /// Verify integrity of multiple cached files
1325    pub fn batch_verify_integrity(&self, files_andhashes: &[(&str, &str)]) -> BatchResult {
1326        let start_time = std::time::Instant::now();
1327        let mut result = BatchResult::new();
1328
1329        for &(filename, expected_hash) in files_andhashes {
1330            match self.cache.cache.get_cachedpath(filename).exists() {
1331                true => match sha256_hash_file(&self.cache.cache.get_cachedpath(filename)) {
1332                    Ok(actual_hash) => {
1333                        if actual_hash == expected_hash {
1334                            result.success_count += 1;
1335                            if let Ok(metadata) =
1336                                std::fs::metadata(self.cache.cache.get_cachedpath(filename))
1337                            {
1338                                result.total_bytes += metadata.len();
1339                            }
1340                        } else {
1341                            result.failure_count += 1;
1342                            result.failures.push((
1343                                filename.to_string(),
1344                                format!(
1345                                    "Hash mismatch: expected {expected_hash}, got {actual_hash}"
1346                                ),
1347                            ));
1348                        }
1349                    }
1350                    Err(e) => {
1351                        result.failure_count += 1;
1352                        result.failures.push((
1353                            filename.to_string(),
1354                            format!("Hash computation failed: {e}"),
1355                        ));
1356                    }
1357                },
1358                false => {
1359                    result.failure_count += 1;
1360                    result
1361                        .failures
1362                        .push((filename.to_string(), "File not found in cache".to_string()));
1363                }
1364            }
1365        }
1366
1367        result.elapsed_time = start_time.elapsed();
1368        result
1369    }
1370
1371    /// Clean up cache selectively based on patterns
1372    pub fn selective_cleanup(
1373        &self,
1374        patterns: &[&str],
1375        max_age_days: Option<u32>,
1376    ) -> Result<BatchResult> {
1377        let start_time = std::time::Instant::now();
1378        let mut result = BatchResult::new();
1379
1380        let cached_files = self.cache.list_cached_files()?;
1381        let now = std::time::SystemTime::now();
1382
1383        for filename in cached_files {
1384            let should_remove = patterns.iter().any(|pattern| {
1385                filename.contains(pattern) || matches_glob_pattern(&filename, pattern)
1386            });
1387
1388            if should_remove {
1389                let filepath = self.cache.cache.get_cachedpath(&filename);
1390
1391                // Check age if max_age_days is specified
1392                let remove_due_to_age = if let Some(max_age) = max_age_days {
1393                    if let Ok(metadata) = std::fs::metadata(&filepath) {
1394                        if let Ok(modified) = metadata.modified() {
1395                            if let Ok(age) = now.duration_since(modified) {
1396                                age.as_secs() > (max_age as u64 * 24 * 3600)
1397                            } else {
1398                                false
1399                            }
1400                        } else {
1401                            false
1402                        }
1403                    } else {
1404                        false
1405                    }
1406                } else {
1407                    true // Remove regardless of age if no age limit specified
1408                };
1409
1410                if remove_due_to_age {
1411                    match self.cache.remove(&filename) {
1412                        Ok(_) => {
1413                            result.success_count += 1;
1414                            if let Ok(metadata) = std::fs::metadata(&filepath) {
1415                                result.total_bytes += metadata.len();
1416                            }
1417                        }
1418                        Err(e) => {
1419                            result.failure_count += 1;
1420                            result
1421                                .failures
1422                                .push((filename, format!("Removal failed: {e}")));
1423                        }
1424                    }
1425                }
1426            }
1427        }
1428
1429        result.elapsed_time = start_time.elapsed();
1430        Ok(result)
1431    }
1432
1433    /// Process multiple datasets with a given function
1434    pub fn batch_process<F, T, E>(&self, names: &[String], processor: F) -> BatchResult
1435    where
1436        F: Fn(&str, &[u8]) -> std::result::Result<T, E> + Sync + Send + 'static,
1437        E: std::fmt::Display,
1438        T: Send,
1439    {
1440        let start_time = std::time::Instant::now();
1441        let mut result = BatchResult::new();
1442
1443        if self.parallel {
1444            self.batch_process_parallel(names, processor, &mut result)
1445        } else {
1446            self.batch_process_sequential(names, processor, &mut result)
1447        }
1448
1449        result.elapsed_time = start_time.elapsed();
1450        result
1451    }
1452
1453    fn batch_process_parallel<F, T, E>(
1454        &self,
1455        names: &[String],
1456        processor: F,
1457        result: &mut BatchResult,
1458    ) where
1459        F: Fn(&str, &[u8]) -> std::result::Result<T, E> + Sync + Send + 'static,
1460        E: std::fmt::Display,
1461        T: Send,
1462    {
1463        // For thread safety with the current cache implementation,
1464        // we need to read all data first, then process in parallel
1465        let mut data_pairs = Vec::new();
1466
1467        // Sequential read phase
1468        for name in names {
1469            match self.cache.cache.read_cached(name) {
1470                Ok(data) => data_pairs.push((name.clone(), data)),
1471                Err(e) => {
1472                    result.failure_count += 1;
1473                    result
1474                        .failures
1475                        .push((name.clone(), format!("Cache read failed: {e}")));
1476                }
1477            }
1478        }
1479
1480        // Parallel processing phase
1481        if !data_pairs.is_empty() {
1482            use std::sync::{Arc, Mutex};
1483            use std::thread;
1484
1485            let parallel_result = Arc::new(Mutex::new(BatchResult::new()));
1486            let processor = Arc::new(processor);
1487
1488            let handles: Vec<_> = data_pairs
1489                .into_iter()
1490                .map(|(name, data)| {
1491                    let result_clone = Arc::clone(&parallel_result);
1492                    let processor_clone = Arc::clone(&processor);
1493
1494                    thread::spawn(move || match processor_clone(&name, &data) {
1495                        Ok(_) => {
1496                            let mut r = result_clone.lock().expect("Operation failed");
1497                            r.success_count += 1;
1498                            r.total_bytes += data.len() as u64;
1499                        }
1500                        Err(e) => {
1501                            let mut r = result_clone.lock().expect("Operation failed");
1502                            r.failure_count += 1;
1503                            r.failures.push((name, format!("Processing failed: {e}")));
1504                        }
1505                    })
1506                })
1507                .collect();
1508
1509            for handle in handles {
1510                let _ = handle.join();
1511            }
1512
1513            // Merge parallel results into main result
1514            let parallel_result = parallel_result.lock().expect("Operation failed");
1515            result.success_count += parallel_result.success_count;
1516            result.failure_count += parallel_result.failure_count;
1517            result.total_bytes += parallel_result.total_bytes;
1518            result.failures.extend(parallel_result.failures.clone());
1519        }
1520    }
1521
1522    fn batch_process_sequential<F, T, E>(
1523        &self,
1524        names: &[String],
1525        processor: F,
1526        result: &mut BatchResult,
1527    ) where
1528        F: Fn(&str, &[u8]) -> std::result::Result<T, E>,
1529        E: std::fmt::Display,
1530    {
1531        for name in names {
1532            match self.cache.cache.read_cached(name) {
1533                Ok(data) => match processor(name, &data) {
1534                    Ok(_) => {
1535                        result.success_count += 1;
1536                        result.total_bytes += data.len() as u64;
1537                    }
1538                    Err(e) => {
1539                        result.failure_count += 1;
1540                        result
1541                            .failures
1542                            .push((name.clone(), format!("Processing failed: {e}")));
1543                    }
1544                },
1545                Err(e) => {
1546                    result.failure_count += 1;
1547                    result
1548                        .failures
1549                        .push((name.clone(), format!("Cache read failed: {e}")));
1550                }
1551            }
1552        }
1553    }
1554
1555    /// Get access to the underlying cache manager
1556    pub fn cache_manager(&self) -> &CacheManager {
1557        &self.cache
1558    }
1559
1560    /// Write data to cache
1561    pub fn write_cached(&self, name: &str, data: &[u8]) -> Result<()> {
1562        self.cache.cache.write_cached(name, data)
1563    }
1564
1565    /// Read data from cache
1566    pub fn read_cached(&self, name: &str) -> Result<Vec<u8>> {
1567        self.cache.cache.read_cached(name)
1568    }
1569
1570    /// List cached files
1571    pub fn list_cached_files(&self) -> Result<Vec<String>> {
1572        self.cache.list_cached_files()
1573    }
1574
1575    /// Print cache report
1576    pub fn print_cache_report(&self) -> Result<()> {
1577        self.cache.print_cache_report()
1578    }
1579
1580    /// Get statistics about cached datasets
1581    pub fn get_cache_statistics(&self) -> Result<BatchResult> {
1582        let start_time = std::time::Instant::now();
1583        let mut result = BatchResult::new();
1584
1585        let cached_files = self.cache.list_cached_files()?;
1586
1587        for filename in cached_files {
1588            let filepath = self.cache.cache.get_cachedpath(&filename);
1589            match std::fs::metadata(&filepath) {
1590                Ok(metadata) => {
1591                    result.success_count += 1;
1592                    result.total_bytes += metadata.len();
1593                }
1594                Err(e) => {
1595                    result.failure_count += 1;
1596                    result
1597                        .failures
1598                        .push((filename, format!("Metadata read failed: {e}")));
1599                }
1600            }
1601        }
1602
1603        result.elapsed_time = start_time.elapsed();
1604        Ok(result)
1605    }
1606}
1607
1608/// Simple glob pattern matching for filenames
1609#[allow(dead_code)]
1610fn matches_glob_pattern(filename: &str, pattern: &str) -> bool {
1611    if pattern == "*" {
1612        return true;
1613    }
1614
1615    if pattern.contains('*') {
1616        let parts: Vec<&str> = pattern.split('*').collect();
1617        if parts.len() == 2 {
1618            let prefix = parts[0];
1619            let suffix = parts[1];
1620            return filename.starts_with(prefix) && filename.ends_with(suffix);
1621        }
1622    }
1623
1624    filename == pattern
1625}
1626
1627#[cfg(test)]
1628mod tests {
1629    use super::*;
1630    use tempfile::TempDir;
1631
1632    #[test]
1633    fn test_batch_result() {
1634        let mut result = BatchResult::new();
1635        assert_eq!(result.success_count, 0);
1636        assert_eq!(result.failure_count, 0);
1637        assert!(result.is_all_success());
1638        assert_eq!(result.success_rate(), 0.0);
1639
1640        result.success_count = 8;
1641        result.failure_count = 2;
1642        result.total_bytes = 1024;
1643
1644        assert!(!result.is_all_success());
1645        assert_eq!(result.success_rate(), 80.0);
1646        assert!(result.summary().contains("8/10 successful"));
1647        assert!(result.summary().contains("80.0%"));
1648    }
1649
1650    #[test]
1651    fn test_batch_operations_creation() {
1652        let tempdir = TempDir::new().expect("Operation failed");
1653        let cache_manager = CacheManager::with_config(tempdir.path().to_path_buf(), 10, 3600);
1654        let batch_ops = BatchOperations::new(cache_manager)
1655            .with_parallel(false)
1656            .with_retry_config(2, std::time::Duration::from_millis(500));
1657
1658        assert!(!batch_ops.parallel);
1659        assert_eq!(batch_ops.max_retries, 2);
1660    }
1661
1662    #[test]
1663    fn test_selective_cleanup() {
1664        let tempdir = TempDir::new().expect("Operation failed");
1665        let cache_manager = CacheManager::with_config(tempdir.path().to_path_buf(), 10, 3600);
1666        let batch_ops = BatchOperations::new(cache_manager);
1667
1668        // Create some test files
1669        let test_data = vec![0u8; 100];
1670        batch_ops
1671            .cache
1672            .cache
1673            .write_cached("test1.csv", &test_data)
1674            .expect("Test: cache operation failed");
1675        batch_ops
1676            .cache
1677            .cache
1678            .write_cached("test2.csv", &test_data)
1679            .expect("Test: cache operation failed");
1680        batch_ops
1681            .cache
1682            .cache
1683            .write_cached("data.json", &test_data)
1684            .expect("Test: cache operation failed");
1685
1686        // Clean up files matching pattern
1687        let result = batch_ops
1688            .selective_cleanup(&["*.csv"], None)
1689            .expect("Operation failed");
1690
1691        assert_eq!(result.success_count, 2); // Should remove test1.csv and test2.csv
1692        assert!(!batch_ops.cache.is_cached("test1.csv"));
1693        assert!(!batch_ops.cache.is_cached("test2.csv"));
1694        assert!(batch_ops.cache.is_cached("data.json")); // Should remain
1695    }
1696
1697    #[test]
1698    fn test_batch_process() {
1699        let tempdir = TempDir::new().expect("Operation failed");
1700        let cache_manager = CacheManager::with_config(tempdir.path().to_path_buf(), 10, 3600);
1701        let batch_ops = BatchOperations::new(cache_manager).with_parallel(false);
1702
1703        // Create test files
1704        let test_data1 = vec![1u8; 100];
1705        let test_data2 = vec![2u8; 200];
1706        batch_ops
1707            .cache
1708            .cache
1709            .write_cached("file1.dat", &test_data1)
1710            .expect("Test: cache operation failed");
1711        batch_ops
1712            .cache
1713            .cache
1714            .write_cached("file2.dat", &test_data2)
1715            .expect("Test: cache operation failed");
1716
1717        let files = vec!["file1.dat".to_string(), "file2.dat".to_string()];
1718
1719        // Process files (verify they're non-empty)
1720        let result = batch_ops.batch_process(&files, |_name, data| {
1721            if data.is_empty() {
1722                Err("Empty file")
1723            } else {
1724                Ok(data.len())
1725            }
1726        });
1727
1728        assert_eq!(result.success_count, 2);
1729        assert_eq!(result.failure_count, 0);
1730        assert_eq!(result.total_bytes, 300); // 100 + 200
1731    }
1732
1733    #[test]
1734    fn test_get_cache_statistics() {
1735        let tempdir = TempDir::new().expect("Operation failed");
1736        let cache_manager = CacheManager::with_config(tempdir.path().to_path_buf(), 10, 3600);
1737        let batch_ops = BatchOperations::new(cache_manager);
1738
1739        // Start with empty cache
1740        let result = batch_ops.get_cache_statistics().expect("Operation failed");
1741        assert_eq!(result.success_count, 0);
1742
1743        // Add some files
1744        let test_data = vec![0u8; 500];
1745        batch_ops
1746            .cache
1747            .cache
1748            .write_cached("test1.dat", &test_data)
1749            .expect("Test: cache operation failed");
1750        batch_ops
1751            .cache
1752            .cache
1753            .write_cached("test2.dat", &test_data)
1754            .expect("Test: cache operation failed");
1755
1756        let result = batch_ops.get_cache_statistics().expect("Operation failed");
1757        assert_eq!(result.success_count, 2);
1758        assert_eq!(result.total_bytes, 1000);
1759    }
1760
1761    #[test]
1762    fn test_matches_glob_pattern() {
1763        assert!(matches_glob_pattern("test.csv", "*"));
1764        assert!(matches_glob_pattern("test.csv", "*.csv"));
1765        assert!(matches_glob_pattern("test.csv", "test.*"));
1766        assert!(matches_glob_pattern("test.csv", "test.csv"));
1767
1768        assert!(!matches_glob_pattern("test.json", "*.csv"));
1769        assert!(!matches_glob_pattern("other.csv", "test.*"));
1770    }
1771
1772    #[test]
1773    fn test_cache_manager_creation() {
1774        let tempdir = TempDir::new().expect("Operation failed");
1775        let manager = CacheManager::with_config(tempdir.path().to_path_buf(), 10, 3600);
1776        let stats = manager.get_stats();
1777        assert_eq!(stats.file_count, 0);
1778    }
1779
1780    #[test]
1781    fn test_cache_stats_formatting() {
1782        let tempdir = TempDir::new().expect("Operation failed");
1783        let stats = CacheStats {
1784            total_size_bytes: 1024,
1785            file_count: 1,
1786            cachedir: tempdir.path().to_path_buf(),
1787        };
1788
1789        assert_eq!(stats.formatted_size(), "1.0 KB");
1790
1791        let stats_large = CacheStats {
1792            total_size_bytes: 1024 * 1024 * 1024,
1793            file_count: 1,
1794            cachedir: tempdir.path().to_path_buf(),
1795        };
1796
1797        assert_eq!(stats_large.formatted_size(), "1.0 GB");
1798    }
1799
1800    #[test]
1801    fn test_hash_file_name() {
1802        let hash1 = DatasetCache::hash_filename("test.csv");
1803        let hash2 = DatasetCache::hash_filename("test.csv");
1804        let hash3 = DatasetCache::hash_filename("different.csv");
1805
1806        assert_eq!(hash1, hash2);
1807        assert_ne!(hash1, hash3);
1808        assert_eq!(hash1.len(), 64); // Blake3 produces 32-byte hashes = 64 hex chars
1809    }
1810
1811    #[test]
1812    fn test_platform_cachedir() {
1813        let cachedir = get_platform_cachedir();
1814        // Should work on any platform
1815        assert!(cachedir.is_some() || cfg!(target_os = "unknown"));
1816
1817        if let Some(dir) = cachedir {
1818            assert!(dir.to_string_lossy().contains("scirs2-datasets"));
1819        }
1820    }
1821
1822    #[test]
1823    fn test_cache_size_management() {
1824        let tempdir = TempDir::new().expect("Operation failed");
1825        let cache = DatasetCache::with_full_config(
1826            tempdir.path().to_path_buf(),
1827            10,
1828            3600,
1829            2048, // 2KB limit
1830            false,
1831        );
1832
1833        // Write multiple small files to approach the limit
1834        let small_data1 = vec![0u8; 400];
1835        cache
1836            .write_cached("small1.dat", &small_data1)
1837            .expect("Operation failed");
1838
1839        let small_data2 = vec![0u8; 400];
1840        cache
1841            .write_cached("small2.dat", &small_data2)
1842            .expect("Operation failed");
1843
1844        let small_data3 = vec![0u8; 400];
1845        cache
1846            .write_cached("small3.dat", &small_data3)
1847            .expect("Operation failed");
1848
1849        // Now write a file that should trigger cleanup
1850        let medium_data = vec![0u8; 800];
1851        cache
1852            .write_cached("medium.dat", &medium_data)
1853            .expect("Operation failed");
1854
1855        // The cache should have cleaned up to stay under the limit
1856        let stats = cache.get_detailed_stats().expect("Operation failed");
1857        assert!(stats.total_size_bytes <= cache.max_cache_size());
1858
1859        // The most recent file should still be cached
1860        assert!(cache.is_cached("medium.dat"));
1861    }
1862
1863    #[test]
1864    fn test_offline_mode() {
1865        let tempdir = TempDir::new().expect("Operation failed");
1866        let mut cache = DatasetCache::new(tempdir.path().to_path_buf());
1867
1868        assert!(!cache.is_offline());
1869        cache.set_offline_mode(true);
1870        assert!(cache.is_offline());
1871    }
1872
1873    #[test]
1874    fn test_detailed_stats() {
1875        let tempdir = TempDir::new().expect("Operation failed");
1876        let cache = DatasetCache::new(tempdir.path().to_path_buf());
1877
1878        let test_data = vec![1, 2, 3, 4, 5];
1879        cache
1880            .write_cached("test.dat", &test_data)
1881            .expect("Operation failed");
1882
1883        let stats = cache.get_detailed_stats().expect("Operation failed");
1884        assert_eq!(stats.file_count, 1);
1885        assert_eq!(stats.total_size_bytes, test_data.len() as u64);
1886        assert_eq!(stats.files.len(), 1);
1887        assert_eq!(stats.files[0].name, "test.dat");
1888        assert_eq!(stats.files[0].size_bytes, test_data.len() as u64);
1889    }
1890
1891    #[test]
1892    fn test_cache_manager() {
1893        let tempdir = TempDir::new().expect("Operation failed");
1894        let manager = CacheManager::with_config(tempdir.path().to_path_buf(), 10, 3600);
1895
1896        let stats = manager.get_stats();
1897        assert_eq!(stats.file_count, 0);
1898        assert_eq!(stats.total_size_bytes, 0);
1899
1900        assert_eq!(manager.cachedir(), &tempdir.path().to_path_buf());
1901    }
1902
1903    #[test]
1904    fn test_format_bytes() {
1905        assert_eq!(format_bytes(512), "512 B");
1906        assert_eq!(format_bytes(1024), "1.0 KB");
1907        assert_eq!(format_bytes(1024 * 1024), "1.0 MB");
1908        assert_eq!(format_bytes(1024 * 1024 * 1024), "1.0 GB");
1909    }
1910}