/**
 * Multi-level Cache Manager
 * 
 * Implements a three-level caching strategy:
 * - Level 1: Hot cache (most accessed nodes) - RAM (automatically detecting and adjusting in each environment)
 * - Level 2: Warm cache (recent nodes) - OPFS, Filesystem or S3 depending on environment
 * - Level 3: Cold storage (all nodes) - OPFS, Filesystem or S3 depending on environment
 */

import { HNSWNoun, GraphVerb, HNSWVerb } from '../coreTypes.js'
import { BrainyError } from '../errors/brainyError.js'

// Extend Navigator interface to include deviceMemory property
// and WorkerGlobalScope to include storage property
declare global {
  interface Navigator {
    deviceMemory?: number;
  }
  
  interface WorkerGlobalScope {
    storage?: {
      getDirectory?: () => Promise<any>;
      [key: string]: any;
    };
  }
}

// Type aliases for better readability
type HNSWNode = HNSWNoun
type Edge = GraphVerb

// Cache entry with metadata for LRU and TTL management
interface CacheEntry<T> {
  data: T
  lastAccessed: number
  accessCount: number
  expiresAt: number | null
}

// Cache statistics for monitoring and tuning
interface CacheStats {
  hits: number
  misses: number
  evictions: number
  size: number
  maxSize: number
  hotCacheSize: number
  warmCacheSize: number
  hotCacheHits: number
  hotCacheMisses: number
  warmCacheHits: number
  warmCacheMisses: number
}

// Environment detection for storage selection
enum Environment {
  BROWSER,
  NODE,
  WORKER
}

// Storage type for warm and cold caches
enum StorageType {
  MEMORY,
  OPFS,
  FILESYSTEM,
  S3,
  REMOTE_API
}

/**
 * Multi-level cache manager for efficient data access
 */
export class CacheManager<T extends HNSWNode | Edge | HNSWVerb> {
  // Hot cache (RAM)
  private hotCache = new Map<string, CacheEntry<T>>()
  
  // Cache statistics
  private stats: CacheStats = {
    hits: 0,
    misses: 0,
    evictions: 0,
    size: 0,
    maxSize: 0,
    hotCacheSize: 0,
    warmCacheSize: 0,
    hotCacheHits: 0,
    hotCacheMisses: 0,
    warmCacheHits: 0,
    warmCacheMisses: 0
  }
  
  // Environment and storage configuration
  private environment: Environment
  private warmStorageType: StorageType
  private coldStorageType: StorageType
  
  // Cache configuration
  private hotCacheMaxSize: number
  private hotCacheEvictionThreshold: number
  private warmCacheTTL: number
  private batchSize: number
  
  // Auto-tuning configuration
  private autoTune: boolean
  private lastAutoTuneTime: number = 0
  private autoTuneInterval: number = 5 * 60 * 1000 // 5 minutes
  private storageStatistics: any = null
  
  // Storage adapters for warm and cold caches
  private warmStorage: any
  private coldStorage: any
  
  // Store options for later reference
  private options: {
    hotCacheMaxSize?: number
    hotCacheEvictionThreshold?: number
    warmCacheTTL?: number
    batchSize?: number
    autoTune?: boolean
    warmStorage?: any
    coldStorage?: any
    readOnly?: boolean
    environmentConfig?: {
      node?: {
        hotCacheMaxSize?: number
        hotCacheEvictionThreshold?: number
        warmCacheTTL?: number
        batchSize?: number
      },
      browser?: {
        hotCacheMaxSize?: number
        hotCacheEvictionThreshold?: number
        warmCacheTTL?: number
        batchSize?: number
      },
      worker?: {
        hotCacheMaxSize?: number
        hotCacheEvictionThreshold?: number
        warmCacheTTL?: number
        batchSize?: number
      },
      [key: string]: {
        hotCacheMaxSize?: number
        hotCacheEvictionThreshold?: number
        warmCacheTTL?: number
        batchSize?: number
      } | undefined
    }
  }
  
  /**
   * Initialize the cache manager
   * @param options Configuration options
   */
  constructor(options: {
    hotCacheMaxSize?: number
    hotCacheEvictionThreshold?: number
    warmCacheTTL?: number
    batchSize?: number
    autoTune?: boolean
    warmStorage?: any
    coldStorage?: any
    readOnly?: boolean
    environmentConfig?: {
      node?: {
        hotCacheMaxSize?: number
        hotCacheEvictionThreshold?: number
        warmCacheTTL?: number
        batchSize?: number
      },
      browser?: {
        hotCacheMaxSize?: number
        hotCacheEvictionThreshold?: number
        warmCacheTTL?: number
        batchSize?: number
      },
      worker?: {
        hotCacheMaxSize?: number
        hotCacheEvictionThreshold?: number
        warmCacheTTL?: number
        batchSize?: number
      },
      [key: string]: {
        hotCacheMaxSize?: number
        hotCacheEvictionThreshold?: number
        warmCacheTTL?: number
        batchSize?: number
      } | undefined
    }
  } = {}) {
    // Store options for later reference
    this.options = options
    
    // Detect environment
    this.environment = this.detectEnvironment()
    
    // Set storage types based on environment
    this.warmStorageType = this.detectWarmStorageType()
    this.coldStorageType = this.detectColdStorageType()
    
    // Initialize storage adapters
    this.warmStorage = options.warmStorage || this.initializeWarmStorage()
    this.coldStorage = options.coldStorage || this.initializeColdStorage()
    
    // Set auto-tuning flag
    this.autoTune = options.autoTune !== undefined ? options.autoTune : true
    
    // Get environment-specific configuration if available
    const envConfig = options.environmentConfig?.[Environment[this.environment].toLowerCase()]
    
    // Set default values or use environment-specific values or global values
    this.hotCacheMaxSize = envConfig?.hotCacheMaxSize || options.hotCacheMaxSize || this.detectOptimalCacheSize()
    this.hotCacheEvictionThreshold = envConfig?.hotCacheEvictionThreshold || options.hotCacheEvictionThreshold || 0.8
    this.warmCacheTTL = envConfig?.warmCacheTTL || options.warmCacheTTL || 24 * 60 * 60 * 1000 // 24 hours
    this.batchSize = envConfig?.batchSize || options.batchSize || 10
    
    // If auto-tuning is enabled, perform initial tuning
    if (this.autoTune) {
      this.tuneParameters()
    }
    
    // Log configuration
    if (process.env.DEBUG) {
      console.log('Cache Manager initialized with configuration:', {
        environment: Environment[this.environment],
        hotCacheMaxSize: this.hotCacheMaxSize,
        hotCacheEvictionThreshold: this.hotCacheEvictionThreshold,
        warmCacheTTL: this.warmCacheTTL,
        batchSize: this.batchSize,
        autoTune: this.autoTune,
        warmStorageType: StorageType[this.warmStorageType],
        coldStorageType: StorageType[this.coldStorageType]
      })
    }
  }
  
  /**
   * Detect the current environment
   */
  private detectEnvironment(): Environment {
    if (typeof window !== 'undefined' && typeof document !== 'undefined') {
      return Environment.BROWSER
    } else if (typeof self !== 'undefined' && typeof window === 'undefined') {
      // In a worker environment, self is defined but window is not
      return Environment.WORKER
    } else {
      return Environment.NODE
    }
  }
  
  /**
   * Detect the optimal cache size based on available memory and operating mode
   * 
   * Enhanced to better handle large datasets in S3 or other storage:
   * - Increases cache size for read-only mode
   * - Adjusts based on total dataset size when available
   * - Provides more aggressive caching for large datasets
   * - Optimizes memory usage based on environment
   */
  private detectOptimalCacheSize(): number {
    try {
      // Default to a conservative value
      const defaultSize = 1000
      
      // Get the total dataset size if available
      const totalItems = this.storageStatistics ? 
        (this.storageStatistics.totalNodes || 0) + (this.storageStatistics.totalEdges || 0) : 0
      
      // Determine if we're dealing with a large dataset (>100K items)
      const isLargeDataset = totalItems > 100000
      
      // Check if we're in read-only mode (from parent Brainy instance)
      const isReadOnly = this.options?.readOnly || false
      
      // In Node.js, use available system memory with enhanced allocation
      if (this.environment === Environment.NODE) {
        try {
          // For ES module compatibility, we'll use a fixed default value
          // since we can't use dynamic imports in a synchronous function
          
          // Use conservative defaults that don't require OS module
          // These values are reasonable for most systems
          const estimatedTotalMemory = 8 * 1024 * 1024 * 1024  // Assume 8GB total
          const estimatedFreeMemory = 4 * 1024 * 1024 * 1024   // Assume 4GB free
          
          // Estimate average entry size (in bytes)
          // This is a conservative estimate for complex objects with vectors
          const ESTIMATED_BYTES_PER_ENTRY = 1024 // 1KB per entry
          
          // Base memory percentage - 10% by default
          let memoryPercentage = 0.1
          
          // Adjust based on operating mode and dataset size
          if (isReadOnly) {
            // In read-only mode, we can use more memory for caching
            memoryPercentage = 0.25 // 25% of free memory
            
            // For large datasets in read-only mode, be even more aggressive
            if (isLargeDataset) {
              memoryPercentage = 0.4 // 40% of free memory
            }
          } else if (isLargeDataset) {
            // For large datasets in normal mode, increase slightly
            memoryPercentage = 0.15 // 15% of free memory
          }
          
          // Calculate optimal size based on adjusted percentage
          const optimalSize = Math.max(
            Math.floor(estimatedFreeMemory * memoryPercentage / ESTIMATED_BYTES_PER_ENTRY),
            1000
          )
          
          // If we know the total dataset size, cap at a reasonable percentage
          if (totalItems > 0) {
            // In read-only mode, we can cache a larger percentage
            const maxPercentage = isReadOnly ? 0.5 : 0.3
            const maxItems = Math.ceil(totalItems * maxPercentage)
            
            // Return the smaller of the two to avoid excessive memory usage
            return Math.min(optimalSize, maxItems)
          }
          
          return optimalSize
        } catch (error) {
          console.warn('Failed to detect optimal cache size:', error)
          return defaultSize
        }
      }
      
      // In browser, use navigator.deviceMemory with enhanced allocation
      if (this.environment === Environment.BROWSER && navigator.deviceMemory) {
        // Base entries per GB
        let entriesPerGB = 500
        
        // Adjust based on operating mode and dataset size
        if (isReadOnly) {
          entriesPerGB = 800 // More aggressive caching in read-only mode
          
          if (isLargeDataset) {
            entriesPerGB = 1000 // Even more aggressive for large datasets
          }
        } else if (isLargeDataset) {
          entriesPerGB = 600 // Slightly more aggressive for large datasets
        }
        
        // Calculate based on device memory
        const browserCacheSize = Math.max(navigator.deviceMemory * entriesPerGB, 1000)
        
        // If we know the total dataset size, cap at a reasonable percentage
        if (totalItems > 0) {
          // In read-only mode, we can cache a larger percentage
          const maxPercentage = isReadOnly ? 0.4 : 0.25
          const maxItems = Math.ceil(totalItems * maxPercentage)
          
          // Return the smaller of the two to avoid excessive memory usage
          return Math.min(browserCacheSize, maxItems)
        }
        
        return browserCacheSize
      }
      
      // For worker environments or when memory detection fails
      if (this.environment === Environment.WORKER) {
        // Workers typically have limited memory, be conservative
        return isReadOnly ? 2000 : 1000
      }
      
      return defaultSize
    } catch (error) {
      console.warn('Error detecting optimal cache size:', error)
      return 1000 // Conservative default
    }
  }
  
  /**
   * Async version of detectOptimalCacheSize that uses dynamic imports
   * to access system information in Node.js environments
   * 
   * This method provides more accurate memory detection by using
   * the OS module's dynamic import in Node.js environments
   */
  private async detectOptimalCacheSizeAsync(): Promise<number> {
    try {
      // Default to a conservative value
      const defaultSize = 1000
      
      // Get the total dataset size if available
      const totalItems = this.storageStatistics ? 
        (this.storageStatistics.totalNodes || 0) + (this.storageStatistics.totalEdges || 0) : 0
      
      // Determine if we're dealing with a large dataset (>100K items)
      const isLargeDataset = totalItems > 100000
      
      // Check if we're in read-only mode (from parent Brainy instance)
      const isReadOnly = this.options?.readOnly || false
      
      // Get memory information based on environment
      const memoryInfo = await this.detectAvailableMemory()
      
      // If memory detection failed, use the synchronous method
      if (!memoryInfo) {
        return this.detectOptimalCacheSize()
      }
      
      // Estimate average entry size (in bytes)
      // This is a conservative estimate for complex objects with vectors
      const ESTIMATED_BYTES_PER_ENTRY = 1024 // 1KB per entry
      
      // Base memory percentage - 10% by default
      let memoryPercentage = 0.1
      
      // Adjust based on operating mode and dataset size
      if (isReadOnly) {
        // In read-only mode, we can use more memory for caching
        memoryPercentage = 0.25 // 25% of free memory
        
        // For large datasets in read-only mode, be even more aggressive
        if (isLargeDataset) {
          memoryPercentage = 0.4 // 40% of free memory
        }
      } else if (isLargeDataset) {
        // For large datasets in normal mode, increase slightly
        memoryPercentage = 0.15 // 15% of free memory
      }
      
      // Calculate optimal size based on adjusted percentage
      const optimalSize = Math.max(
        Math.floor(memoryInfo.freeMemory * memoryPercentage / ESTIMATED_BYTES_PER_ENTRY),
        1000
      )
      
      // If we know the total dataset size, cap at a reasonable percentage
      if (totalItems > 0) {
        // In read-only mode, we can cache a larger percentage
        const maxPercentage = isReadOnly ? 0.5 : 0.3
        const maxItems = Math.ceil(totalItems * maxPercentage)
        
        // Return the smaller of the two to avoid excessive memory usage
        return Math.min(optimalSize, maxItems)
      }
      
      return optimalSize
    } catch (error) {
      console.warn('Error detecting optimal cache size asynchronously:', error)
      return 1000 // Conservative default
    }
  }
  
  /**
   * Detects available memory across different environments
   * 
   * This method uses different techniques to detect memory in:
   * - Node.js: Uses the OS module with dynamic import
   * - Browser: Uses performance.memory or navigator.deviceMemory
   * - Worker: Uses performance.memory if available
   * 
   * @returns An object with totalMemory and freeMemory in bytes, or null if detection fails
   */
  private async detectAvailableMemory(): Promise<{ totalMemory: number, freeMemory: number } | null> {
    try {
      // Node.js environment
      if (this.environment === Environment.NODE) {
        try {
          // Use dynamic import for OS module
          const os = await import('node:os')
          
          // Get actual system memory information
          const totalMemory = os.totalmem()
          const freeMemory = os.freemem()
          
          return { totalMemory, freeMemory }
        } catch (error) {
          console.warn('Failed to detect memory in Node.js environment:', error)
        }
      }
      
      // Browser environment
      if (this.environment === Environment.BROWSER) {
        // Try using performance.memory (Chrome only)
        if (performance && (performance as any).memory) {
          const memoryInfo = (performance as any).memory
          
          // jsHeapSizeLimit is the maximum size of the heap
          // totalJSHeapSize is the currently allocated heap size
          // usedJSHeapSize is the amount of heap currently being used
          const totalMemory = memoryInfo.jsHeapSizeLimit || 0
          const usedMemory = memoryInfo.usedJSHeapSize || 0
          const freeMemory = Math.max(totalMemory - usedMemory, 0)
          
          return { totalMemory, freeMemory }
        }
        
        // Try using navigator.deviceMemory as fallback
        if (navigator.deviceMemory) {
          // deviceMemory is in GB, convert to bytes
          const totalMemory = navigator.deviceMemory * 1024 * 1024 * 1024
          // Assume 50% is free
          const freeMemory = totalMemory * 0.5
          
          return { totalMemory, freeMemory }
        }
      }
      
      // Worker environment
      if (this.environment === Environment.WORKER) {
        // Try using performance.memory if available (Chrome workers)
        if (performance && (performance as any).memory) {
          const memoryInfo = (performance as any).memory
          
          const totalMemory = memoryInfo.jsHeapSizeLimit || 0
          const usedMemory = memoryInfo.usedJSHeapSize || 0
          const freeMemory = Math.max(totalMemory - usedMemory, 0)
          
          return { totalMemory, freeMemory }
        }
        
        // For workers, use a conservative estimate
        // Assume 2GB total memory with 1GB free
        return {
          totalMemory: 2 * 1024 * 1024 * 1024,
          freeMemory: 1 * 1024 * 1024 * 1024
        }
      }
      
      // If all detection methods fail, use conservative defaults
      return {
        totalMemory: 8 * 1024 * 1024 * 1024,  // Assume 8GB total
        freeMemory: 4 * 1024 * 1024 * 1024    // Assume 4GB free
      }
    } catch (error) {
      console.warn('Memory detection failed:', error)
      return null
    }
  }
  
  /**
   * Tune cache parameters based on statistics and environment
   * This method is called periodically if auto-tuning is enabled
   * 
   * The auto-tuning process:
   * 1. Retrieves storage statistics if available
   * 2. Tunes each parameter based on statistics and environment
   * 3. Logs the tuned parameters if debug is enabled
   * 
   * Auto-tuning helps optimize cache performance by adapting to:
   * - The current environment (Node.js, browser, worker)
   * - Available system resources (memory, CPU)
   * - Usage patterns (read-heavy vs. write-heavy workloads)
   * - Cache efficiency (hit/miss ratios)
   */
  private async tuneParameters(): Promise<void> {
    // Skip if auto-tuning is disabled
    if (!this.autoTune) return
    
    // Check if it's time to tune parameters
    const now = Date.now()
    if (now - this.lastAutoTuneTime < this.autoTuneInterval) return
    
    // Update last tune time
    this.lastAutoTuneTime = now
    
    try {
      // Get storage statistics if available
      if (this.coldStorage && typeof this.coldStorage.getStatistics === 'function') {
        this.storageStatistics = await this.coldStorage.getStatistics()
      }
      
      // Get cache statistics for adaptive tuning
      const cacheStats = this.getStats()
      
      // Use the async version of tuneHotCacheSize which uses detectOptimalCacheSizeAsync
      await this.tuneHotCacheSize()
      
      // Tune eviction threshold based on hit/miss ratio
      this.tuneEvictionThreshold(cacheStats)
      
      // Tune warm cache TTL based on access patterns
      this.tuneWarmCacheTTL(cacheStats)
      
      // Tune batch size based on access patterns and storage type
      this.tuneBatchSize(cacheStats)
      
      // Log tuned parameters if debug is enabled
      if (process.env.DEBUG) {
        console.log('Cache parameters auto-tuned:', {
          hotCacheMaxSize: this.hotCacheMaxSize,
          hotCacheEvictionThreshold: this.hotCacheEvictionThreshold,
          warmCacheTTL: this.warmCacheTTL,
          batchSize: this.batchSize,
          cacheStats: {
            hotCacheSize: cacheStats.hotCacheSize,
            warmCacheSize: cacheStats.warmCacheSize,
            hotCacheHits: cacheStats.hotCacheHits,
            hotCacheMisses: cacheStats.hotCacheMisses,
            warmCacheHits: cacheStats.warmCacheHits,
            warmCacheMisses: cacheStats.warmCacheMisses
          }
        })
      }
    } catch (error) {
      console.warn('Error during cache parameter auto-tuning:', error)
    }
  }
  
  /**
   * Tune hot cache size based on statistics, environment, and operating mode
   * 
   * The hot cache size is tuned based on:
   * 1. Available memory in the current environment
   * 2. Total number of nodes and edges in the system
   * 3. Cache hit/miss ratio
   * 4. Operating mode (read-only vs. read-write)
   * 5. Storage type (S3, filesystem, memory)
   * 
   * Enhanced algorithm:
   * - Start with a size based on available memory and operating mode
   * - For large datasets in S3 or other remote storage, use more aggressive caching
   * - Adjust based on access patterns (read-heavy vs. write-heavy)
   * - For read-only mode, prioritize cache size over eviction speed
   * - Dynamically adjust based on hit/miss ratio and query patterns
   */
  private async tuneHotCacheSize(): Promise<void> {
    // Use the async version to get more accurate memory information
    let optimalSize = await this.detectOptimalCacheSizeAsync()
    
    // Check if we're in read-only mode
    const isReadOnly = this.options?.readOnly || false
    
    // Check if we're using S3 or other remote storage
    const isRemoteStorage = 
      this.coldStorageType === StorageType.S3 || 
      this.coldStorageType === StorageType.REMOTE_API
    
    // If we have storage statistics, adjust based on total nodes/edges
    if (this.storageStatistics) {
      const totalItems = (this.storageStatistics.totalNodes || 0) + 
                         (this.storageStatistics.totalEdges || 0)
      
      // If total items is significant, adjust cache size
      if (totalItems > 0) {
        // Base percentage to cache - adjusted based on mode and storage
        let percentageToCache = 0.2 // Cache 20% of items by default
        
        // For read-only mode, increase cache percentage
        if (isReadOnly) {
          percentageToCache = 0.3 // 30% for read-only mode
          
          // For remote storage in read-only mode, be even more aggressive
          if (isRemoteStorage) {
            percentageToCache = 0.4 // 40% for remote storage in read-only mode
          }
        } 
        // For remote storage in normal mode, increase slightly
        else if (isRemoteStorage) {
          percentageToCache = 0.25 // 25% for remote storage
        }
        
        // For large datasets, cap the percentage to avoid excessive memory usage
        if (totalItems > 1000000) { // Over 1 million items
          percentageToCache = Math.min(percentageToCache, 0.15)
        } else if (totalItems > 100000) { // Over 100K items
          percentageToCache = Math.min(percentageToCache, 0.25)
        }
        
        const statisticsBasedSize = Math.ceil(totalItems * percentageToCache)
        
        // Use the smaller of the two to avoid memory issues
        optimalSize = Math.min(optimalSize, statisticsBasedSize)
      }
    }
    
    // Adjust based on hit/miss ratio if we have enough data
    const totalAccesses = this.stats.hits + this.stats.misses
    if (totalAccesses > 100) {
      const hitRatio = this.stats.hits / totalAccesses
      
      // Base adjustment factor
      let hitRatioFactor = 1.0
      
      // If hit ratio is low, we might need a larger cache
      if (hitRatio < 0.5) {
        // Calculate adjustment factor based on hit ratio
        const baseAdjustment = 0.5 - hitRatio
        
        // For read-only mode or remote storage, be more aggressive
        if (isReadOnly || isRemoteStorage) {
          hitRatioFactor = 1 + (baseAdjustment * 1.5) // Up to 75% increase
        } else {
          hitRatioFactor = 1 + baseAdjustment // Up to 50% increase
        }
        
        optimalSize = Math.ceil(optimalSize * hitRatioFactor)
      }
      // If hit ratio is very high, we might be able to reduce cache size slightly
      else if (hitRatio > 0.9 && !isReadOnly && !isRemoteStorage) {
        // Only reduce cache size in normal mode with local storage
        // and only if hit ratio is very high
        hitRatioFactor = 0.9 // 10% reduction
        optimalSize = Math.ceil(optimalSize * hitRatioFactor)
      }
    }
    
    // Check for operation patterns if available
    if (this.storageStatistics?.operations) {
      const ops = this.storageStatistics.operations
      const totalOps = ops.total || 1
      
      // Calculate read/write ratio
      const readOps = (ops.search || 0) + (ops.get || 0)
      const writeOps = (ops.add || 0) + (ops.update || 0) + (ops.delete || 0)
      
      if (totalOps > 100) {
        const readRatio = readOps / totalOps
        
        // For read-heavy workloads, increase cache size
        if (readRatio > 0.8) {
          // More aggressive for remote storage
          const readAdjustment = isRemoteStorage ? 1.3 : 1.2
          optimalSize = Math.ceil(optimalSize * readAdjustment)
        }
      }
    }
    
    // Ensure we have a reasonable minimum size based on environment and mode
    let minSize = 1000 // Default minimum
    
    // For read-only mode, use a higher minimum
    if (isReadOnly) {
      minSize = 2000
    }
    
    // For remote storage, use an even higher minimum
    if (isRemoteStorage) {
      minSize = isReadOnly ? 3000 : 2000
    }
    
    optimalSize = Math.max(optimalSize, minSize)
    
    // Update the hot cache max size
    this.hotCacheMaxSize = optimalSize
    this.stats.maxSize = optimalSize
  }
  
  /**
   * Tune eviction threshold based on statistics
   * 
   * The eviction threshold determines when items start being evicted from the hot cache.
   * It is tuned based on:
   * 1. Cache hit/miss ratio
   * 2. Operation patterns (read-heavy vs. write-heavy workloads)
   * 3. Memory pressure and available resources
   * 
   * Algorithm:
   * - Start with a default threshold of 0.8 (80% of max size)
   * - For high hit ratios, increase the threshold to keep more items in cache
   * - For low hit ratios, decrease the threshold to evict items more aggressively
   * - For read-heavy workloads, use a higher threshold
   * - For write-heavy workloads, use a lower threshold
   * - Under memory pressure, use a lower threshold to conserve resources
   * 
   * @param cacheStats Optional cache statistics for more adaptive tuning
   */
  private tuneEvictionThreshold(cacheStats?: CacheStats): void {
    // Default threshold
    let threshold = 0.8
    
    // Use provided cache stats or internal stats
    const stats = cacheStats || this.getStats()
    
    // Adjust based on hit/miss ratio if we have enough data
    const totalHotAccesses = stats.hotCacheHits + stats.hotCacheMisses
    if (totalHotAccesses > 100) {
      const hotHitRatio = stats.hotCacheHits / totalHotAccesses
      
      // If hit ratio is high, we can use a higher threshold
      // If hit ratio is low, we should use a lower threshold to evict more aggressively
      if (hotHitRatio > 0.8) {
        // High hit ratio, increase threshold (up to 0.9)
        threshold = Math.min(0.9, 0.8 + (hotHitRatio - 0.8) * 0.5)
      } else if (hotHitRatio < 0.5) {
        // Low hit ratio, decrease threshold (down to 0.6)
        threshold = Math.max(0.6, 0.8 - (0.5 - hotHitRatio) * 0.5)
      }
    }
    
    // If we have storage statistics with operation counts, adjust based on operation patterns
    if (this.storageStatistics && this.storageStatistics.operations) {
      const ops = this.storageStatistics.operations
      const totalOps = ops.total || 1
      
      // Calculate read/write ratio
      const readOps = ops.search || 0
      const writeOps = (ops.add || 0) + (ops.update || 0) + (ops.delete || 0)
      
      if (totalOps > 100) {
        const readRatio = readOps / totalOps
        const writeRatio = writeOps / totalOps
        
        // For read-heavy workloads, use higher threshold
        // For write-heavy workloads, use lower threshold
        if (readRatio > 0.8) {
          // Read-heavy, increase threshold slightly
          threshold = Math.min(0.9, threshold + 0.05)
        } else if (writeRatio > 0.5) {
          // Write-heavy, decrease threshold
          threshold = Math.max(0.6, threshold - 0.1)
        }
      }
    }
    
    // Check memory pressure - if hot cache is growing too fast relative to hits,
    // reduce the threshold to conserve memory
    if (stats.hotCacheSize > 0 && totalHotAccesses > 0) {
      const sizeToAccessRatio = stats.hotCacheSize / totalHotAccesses
      
      // If the ratio is high, it means we're caching a lot but not getting many hits
      if (sizeToAccessRatio > 10) {
        // Reduce threshold more aggressively under high memory pressure
        threshold = Math.max(0.5, threshold - 0.1)
      }
    }
    
    // If we're in read-only mode, we can be more aggressive with caching
    const isReadOnly = this.options?.readOnly || false
    if (isReadOnly) {
      threshold = Math.min(0.95, threshold + 0.05)
    }
    
    // Update the eviction threshold
    this.hotCacheEvictionThreshold = threshold
  }
  
  /**
   * Tune warm cache TTL based on statistics
   * 
   * The warm cache TTL determines how long items remain in the warm cache.
   * It is tuned based on:
   * 1. Update frequency from operation statistics
   * 2. Warm cache hit/miss ratio
   * 3. Access patterns and frequency
   * 4. Available storage resources
   * 
   * Algorithm:
   * - Start with a default TTL of 24 hours
   * - For frequently updated data, use a shorter TTL
   * - For rarely updated data, use a longer TTL
   * - For frequently accessed data, use a longer TTL
   * - For rarely accessed data, use a shorter TTL
   * - Under storage pressure, use a shorter TTL
   * 
   * @param cacheStats Optional cache statistics for more adaptive tuning
   */
  private tuneWarmCacheTTL(cacheStats?: CacheStats): void {
    // Default TTL (24 hours)
    let ttl = 24 * 60 * 60 * 1000
    
    // Use provided cache stats or internal stats
    const stats = cacheStats || this.getStats()
    
    // Adjust based on warm cache hit/miss ratio if we have enough data
    const totalWarmAccesses = stats.warmCacheHits + stats.warmCacheMisses
    if (totalWarmAccesses > 50) {
      const warmHitRatio = stats.warmCacheHits / totalWarmAccesses
      
      // If warm cache hit ratio is high, items in warm cache are useful
      // so we should keep them longer
      if (warmHitRatio > 0.7) {
        // High hit ratio, increase TTL (up to 36 hours)
        ttl = Math.min(36 * 60 * 60 * 1000, ttl * (1 + (warmHitRatio - 0.7)))
      } else if (warmHitRatio < 0.3) {
        // Low hit ratio, decrease TTL (down to 12 hours)
        ttl = Math.max(12 * 60 * 60 * 1000, ttl * (0.8 - (0.3 - warmHitRatio)))
      }
    }
    
    // If we have storage statistics with operation counts, adjust based on update frequency
    if (this.storageStatistics && this.storageStatistics.operations) {
      const ops = this.storageStatistics.operations
      const totalOps = ops.total || 1
      const updateOps = (ops.update || 0)
      
      if (totalOps > 100) {
        const updateRatio = updateOps / totalOps
        
        // For frequently updated data, use shorter TTL
        // For rarely updated data, use longer TTL
        if (updateRatio > 0.3) {
          // Frequently updated, decrease TTL (down to 6 hours)
          ttl = Math.max(6 * 60 * 60 * 1000, ttl * (1 - updateRatio * 0.5))
        } else if (updateRatio < 0.1) {
          // Rarely updated, increase TTL (up to 48 hours)
          ttl = Math.min(48 * 60 * 60 * 1000, ttl * (1.2 - updateRatio))
        }
      }
    }
    
    // Check warm cache size relative to hot cache size
    // If warm cache is much larger than hot cache, reduce TTL to prevent excessive storage use
    if (stats.warmCacheSize > 0 && stats.hotCacheSize > 0) {
      const warmToHotRatio = stats.warmCacheSize / stats.hotCacheSize
      
      if (warmToHotRatio > 5) {
        // Warm cache is much larger than hot cache, reduce TTL
        ttl = Math.max(6 * 60 * 60 * 1000, ttl * (0.9 - Math.min(0.3, (warmToHotRatio - 5) / 20)))
      }
    }
    
    // If we're in read-only mode, we can use a longer TTL
    const isReadOnly = this.options?.readOnly || false
    if (isReadOnly) {
      ttl = Math.min(72 * 60 * 60 * 1000, ttl * 1.5)
    }
    
    // Update the warm cache TTL
    this.warmCacheTTL = ttl
  }
  
  /**
   * Tune batch size based on environment, statistics, and operating mode
   * 
   * The batch size determines how many items are processed in a single batch
   * for operations like prefetching. It is tuned based on:
   * 1. Current environment (Node.js, browser, worker)
   * 2. Available memory
   * 3. Operation patterns
   * 4. Cache hit/miss ratio
   * 5. Operating mode (read-only vs. read-write)
   * 6. Storage type (S3, filesystem, memory)
   * 7. Dataset size
   * 8. Cache efficiency and access patterns
   * 
   * Enhanced algorithm:
   * - Start with a default based on the environment
   * - For large datasets in S3 or other remote storage, use larger batches
   * - For read-only mode, use larger batches to improve throughput
   * - Dynamically adjust based on network latency and throughput
   * - Balance between memory usage and performance
   * - Adapt to cache hit/miss patterns
   * 
   * @param cacheStats Optional cache statistics for more adaptive tuning
   */
  private tuneBatchSize(cacheStats?: CacheStats): void {
    // Default batch size
    let batchSize = 10
    
    // Use provided cache stats or internal stats
    const stats = cacheStats || this.getStats()
    
    // Check if we're in read-only mode
    const isReadOnly = this.options?.readOnly || false
    
    // Check if we're using S3 or other remote storage
    const isRemoteStorage = 
      this.coldStorageType === StorageType.S3 || 
      this.coldStorageType === StorageType.REMOTE_API
    
    // Get the total dataset size if available
    const totalItems = this.storageStatistics ? 
      (this.storageStatistics.totalNodes || 0) + (this.storageStatistics.totalEdges || 0) : 0
    
    // Determine if we're dealing with a large dataset
    const isLargeDataset = totalItems > 100000
    const isVeryLargeDataset = totalItems > 1000000
    
    // Base batch size adjustment based on environment
    if (this.environment === Environment.NODE) {
      // Node.js can handle larger batches
      batchSize = isReadOnly ? 30 : 20
      
      // For remote storage, increase batch size
      if (isRemoteStorage) {
        batchSize = isReadOnly ? 50 : 30
      }
      
      // For large datasets, adjust batch size
      if (isLargeDataset) {
        batchSize = Math.min(100, batchSize * 1.5)
      }
      
      // For very large datasets, adjust even more
      if (isVeryLargeDataset) {
        batchSize = Math.min(200, batchSize * 2)
      }
    } else if (this.environment === Environment.BROWSER) {
      // Browsers might need smaller batches
      batchSize = isReadOnly ? 15 : 10
      
      // If we have memory information, adjust accordingly
      if (navigator.deviceMemory) {
        // Scale batch size with available memory
        const memoryFactor = isReadOnly ? 3 : 2
        batchSize = Math.max(5, Math.min(30, Math.floor(navigator.deviceMemory * memoryFactor)))
        
        // For large datasets, adjust based on memory
        if (isLargeDataset && navigator.deviceMemory > 4) {
          batchSize = Math.min(50, batchSize * 1.5)
        }
      }
    } else if (this.environment === Environment.WORKER) {
      // Workers can handle moderate batch sizes
      batchSize = isReadOnly ? 20 : 15
    }
    
    // Adjust based on cache hit/miss ratios
    const totalHotAccesses = stats.hotCacheHits + stats.hotCacheMisses
    const totalWarmAccesses = stats.warmCacheHits + stats.warmCacheMisses
    
    if (totalHotAccesses > 100) {
      const hotHitRatio = stats.hotCacheHits / totalHotAccesses
      
      // If hot cache hit ratio is high, we're effectively using the cache
      // so we can use larger batches for better throughput
      if (hotHitRatio > 0.8) {
        // High hit ratio, increase batch size
        batchSize = Math.min(batchSize * 1.5, isRemoteStorage ? 250 : 150)
      } else if (hotHitRatio < 0.4) {
        // Low hit ratio, we might be fetching too much at once
        // Reduce batch size to be more selective
        batchSize = Math.max(5, batchSize * 0.8)
      }
    }
    
    if (totalWarmAccesses > 50) {
      const warmHitRatio = stats.warmCacheHits / totalWarmAccesses
      
      // If warm cache hit ratio is high, prefetching is effective
      // so we can use larger batches
      if (warmHitRatio > 0.7) {
        // High warm hit ratio, increase batch size
        batchSize = Math.min(batchSize * 1.3, isRemoteStorage ? 200 : 120)
      } else if (warmHitRatio < 0.3) {
        // Low warm hit ratio, reduce batch size
        batchSize = Math.max(5, batchSize * 0.9)
      }
    }
    
    // If we have storage statistics with operation counts, adjust based on operation patterns
    if (this.storageStatistics && this.storageStatistics.operations) {
      const ops = this.storageStatistics.operations
      const totalOps = ops.total || 1
      const searchOps = (ops.search || 0)
      const getOps = (ops.get || 0)
      
      if (totalOps > 100) {
        // Calculate search and get ratios
        const searchRatio = searchOps / totalOps
        const getRatio = getOps / totalOps
        
        // For search-heavy workloads, use larger batch size
        if (searchRatio > 0.6) {
          // Search-heavy, increase batch size
          const searchFactor = isRemoteStorage ? 1.8 : 1.5
          batchSize = Math.min(isRemoteStorage ? 200 : 100, Math.ceil(batchSize * searchFactor))
        }
        
        // For get-heavy workloads, adjust batch size
        if (getRatio > 0.6) {
          // Get-heavy, adjust batch size based on storage type
          if (isRemoteStorage) {
            // For remote storage, larger batches reduce network overhead
            batchSize = Math.min(150, Math.ceil(batchSize * 1.5))
          } else {
            // For local storage, smaller batches might be more efficient
            batchSize = Math.max(10, Math.ceil(batchSize * 0.9))
          }
        }
      }
    }
    
    // Check if we're experiencing memory pressure
    if (stats.hotCacheSize > 0 && this.hotCacheMaxSize > 0) {
      const cacheUtilization = stats.hotCacheSize / this.hotCacheMaxSize
      
      // If cache utilization is high, reduce batch size to avoid memory pressure
      if (cacheUtilization > 0.85) {
        batchSize = Math.max(5, Math.floor(batchSize * 0.8))
      }
    }
    
    // Adjust based on overall hit/miss ratio if we have enough data
    const totalAccesses = stats.hotCacheHits + stats.hotCacheMisses + stats.warmCacheHits + stats.warmCacheMisses
    if (totalAccesses > 100) {
      const hitRatio = (stats.hotCacheHits + stats.warmCacheHits) / totalAccesses
      
      // Base adjustment factors
      let increaseFactorForLowHitRatio = isRemoteStorage ? 1.5 : 1.2
      let decreaseFactorForHighHitRatio = 0.8
      
      // In read-only mode, be more aggressive with batch size adjustments
      if (isReadOnly) {
        increaseFactorForLowHitRatio = isRemoteStorage ? 2.0 : 1.5
        decreaseFactorForHighHitRatio = 0.9 // Less reduction in read-only mode
      }
      
      // If hit ratio is high, we can use smaller batches
      if (hitRatio > 0.8 && !isVeryLargeDataset) {
        // High hit ratio, decrease batch size slightly
        // But don't decrease too much for large datasets or remote storage
        if (!(isLargeDataset && isRemoteStorage)) {
          batchSize = Math.max(isReadOnly ? 10 : 5, Math.floor(batchSize * decreaseFactorForHighHitRatio))
        }
      } 
      // If hit ratio is low, we need larger batches
      else if (hitRatio < 0.5) {
        // Low hit ratio, increase batch size
        const maxBatchSize = isRemoteStorage ? 
          (isVeryLargeDataset ? 300 : 200) : 
          (isVeryLargeDataset ? 150 : 100)
        
        batchSize = Math.min(maxBatchSize, Math.ceil(batchSize * increaseFactorForLowHitRatio))
      }
    }
    
    // Set minimum batch sizes based on storage type and mode
    let minBatchSize = 5
    
    if (isRemoteStorage) {
      minBatchSize = isReadOnly ? 20 : 10
    } else if (isReadOnly) {
      minBatchSize = 10
    }
    
    // Ensure batch size is within reasonable limits
    batchSize = Math.max(minBatchSize, batchSize)
    
    // Cap maximum batch size based on environment and storage
    const maxBatchSize = isRemoteStorage ? 
      (this.environment === Environment.NODE ? 300 : 150) : 
      (this.environment === Environment.NODE ? 150 : 75)
    
    batchSize = Math.min(maxBatchSize, batchSize)
    
    // Update the batch size with the adaptively tuned value
    this.batchSize = Math.round(batchSize)
  }
  
  /**
   * Detect the appropriate warm storage type based on environment
   */
  private detectWarmStorageType(): StorageType {
    if (this.environment === Environment.BROWSER) {
      // Use OPFS if available, otherwise use memory
      if ('storage' in navigator && 'getDirectory' in navigator.storage) {
        return StorageType.OPFS
      }
      return StorageType.MEMORY
    } else if (this.environment === Environment.WORKER) {
      // Use OPFS if available, otherwise use memory
      if ('storage' in self && 'getDirectory' in (self as WorkerGlobalScope).storage!) {
        return StorageType.OPFS
      }
      return StorageType.MEMORY
    } else {
      // In Node.js, use filesystem
      return StorageType.FILESYSTEM
    }
  }
  
  /**
   * Detect the appropriate cold storage type based on environment
   */
  private detectColdStorageType(): StorageType {
    if (this.environment === Environment.BROWSER) {
      // Use OPFS if available, otherwise use memory
      if ('storage' in navigator && 'getDirectory' in navigator.storage) {
        return StorageType.OPFS
      }
      return StorageType.MEMORY
    } else if (this.environment === Environment.WORKER) {
      // Use OPFS if available, otherwise use memory
      if ('storage' in self && 'getDirectory' in (self as WorkerGlobalScope).storage!) {
        return StorageType.OPFS
      }
      return StorageType.MEMORY
    } else {
      // In Node.js, use S3 if configured, otherwise filesystem
      return StorageType.S3
    }
  }
  
  /**
   * Initialize warm storage adapter
   */
  private initializeWarmStorage(): any {
    // Implementation depends on the detected storage type
    // For now, return null as this will be provided by the storage adapter
    return null
  }
  
  /**
   * Initialize cold storage adapter
   */
  private initializeColdStorage(): any {
    // Implementation depends on the detected storage type
    // For now, return null as this will be provided by the storage adapter
    return null
  }
  
  /**
   * Get an item from cache, trying each level in order
   * @param id The item ID
   * @returns The cached item or null if not found
   */
  public async get(id: string): Promise<T | null> {
    // Check if it's time to tune parameters
    await this.checkAndTuneParameters()
    
    // Try hot cache first (fastest)
    const hotCacheEntry = this.hotCache.get(id)
    if (hotCacheEntry) {
      // Update access metadata
      hotCacheEntry.lastAccessed = Date.now()
      hotCacheEntry.accessCount++
      
      // Update stats
      this.stats.hits++
      
      return hotCacheEntry.data
    }
    
    // Try warm cache next
    try {
      const warmCacheItem = await this.getFromWarmCache(id)
      if (warmCacheItem) {
        // Promote to hot cache
        this.addToHotCache(id, warmCacheItem)
        
        // Update stats
        this.stats.hits++
        
        return warmCacheItem
      }
    } catch (error) {
      console.warn(`Error accessing warm cache for ${id}:`, error)
    }
    
    // Finally, try cold storage
    try {
      const coldStorageItem = await this.getFromColdStorage(id)
      if (coldStorageItem) {
        // Promote to hot and warm caches
        this.addToHotCache(id, coldStorageItem)
        await this.addToWarmCache(id, coldStorageItem)
        
        // Update stats
        this.stats.misses++
        
        return coldStorageItem
      }
    } catch (error) {
      console.warn(`Error accessing cold storage for ${id}:`, error)
    }
    
    // Item not found in any cache level
    this.stats.misses++
    return null
  }
  
  /**
   * Get an item from warm cache
   * @param id The item ID
   * @returns The cached item or null if not found
   */
  private async getFromWarmCache(id: string): Promise<T | null> {
    if (!this.warmStorage) return null
    
    try {
      return await this.warmStorage.get(id)
    } catch (error) {
      console.warn(`Error getting item ${id} from warm cache:`, error)
      return null
    }
  }
  
  /**
   * Get an item from cold storage
   * @param id The item ID
   * @returns The item or null if not found
   */
  private async getFromColdStorage(id: string): Promise<T | null> {
    if (!this.coldStorage) return null
    
    try {
      return await this.coldStorage.get(id)
    } catch (error) {
      console.warn(`Error getting item ${id} from cold storage:`, error)
      return null
    }
  }
  
  /**
   * Add an item to hot cache
   * @param id The item ID
   * @param item The item to cache
   */
  private addToHotCache(id: string, item: T): void {
    // Check if we need to evict items
    if (this.hotCache.size >= this.hotCacheMaxSize * this.hotCacheEvictionThreshold) {
      this.evictFromHotCache()
    }
    
    // Add to hot cache
    this.hotCache.set(id, {
      data: item,
      lastAccessed: Date.now(),
      accessCount: 1,
      expiresAt: null // Hot cache items don't expire
    })
    
    // Update stats
    this.stats.size = this.hotCache.size
  }
  
  /**
   * Add an item to warm cache
   * @param id The item ID
   * @param item The item to cache
   */
  private async addToWarmCache(id: string, item: T): Promise<void> {
    if (!this.warmStorage) return
    
    try {
      // Add to warm cache with TTL
      await this.warmStorage.set(id, item, {
        ttl: this.warmCacheTTL
      })
    } catch (error) {
      console.warn(`Error adding item ${id} to warm cache:`, error)
    }
  }
  
  /**
   * Evict items from hot cache based on LRU policy
   */
  private evictFromHotCache(): void {
    // Find the least recently used items
    const entries = Array.from(this.hotCache.entries())
    
    // Sort by last accessed time (oldest first)
    entries.sort((a, b) => a[1].lastAccessed - b[1].lastAccessed)
    
    // Remove the oldest 20% of items
    const itemsToRemove = Math.ceil(this.hotCache.size * 0.2)
    for (let i = 0; i < itemsToRemove && i < entries.length; i++) {
      this.hotCache.delete(entries[i][0])
      this.stats.evictions++
    }
    
    // Update stats
    this.stats.size = this.hotCache.size
    
    if (process.env.DEBUG) {
      console.log(`Evicted ${itemsToRemove} items from hot cache, new size: ${this.hotCache.size}`)
    }
  }
  
  /**
   * Set an item in all cache levels
   * @param id The item ID
   * @param item The item to cache
   */
  public async set(id: string, item: T): Promise<void> {
    // Add to hot cache
    this.addToHotCache(id, item)
    
    // Add to warm cache
    await this.addToWarmCache(id, item)
    
    // Add to cold storage
    if (this.coldStorage) {
      try {
        await this.coldStorage.set(id, item)
      } catch (error) {
        console.warn(`Error adding item ${id} to cold storage:`, error)
      }
    }
  }
  
  /**
   * Delete an item from all cache levels
   * @param id The item ID to delete
   */
  public async delete(id: string): Promise<void> {
    // Remove from hot cache
    this.hotCache.delete(id)
    
    // Remove from warm cache
    if (this.warmStorage) {
      try {
        await this.warmStorage.delete(id)
      } catch (error) {
        console.warn(`Error deleting item ${id} from warm cache:`, error)
      }
    }
    
    // Remove from cold storage
    if (this.coldStorage) {
      try {
        await this.coldStorage.delete(id)
      } catch (error) {
        console.warn(`Error deleting item ${id} from cold storage:`, error)
      }
    }
    
    // Update stats
    this.stats.size = this.hotCache.size
  }
  
  /**
   * Clear all cache levels
   */
  public async clear(): Promise<void> {
    // Clear hot cache
    this.hotCache.clear()
    
    // Clear warm cache
    if (this.warmStorage) {
      try {
        await this.warmStorage.clear()
      } catch (error) {
        console.warn('Error clearing warm cache:', error)
      }
    }
    
    // Clear cold storage
    if (this.coldStorage) {
      try {
        await this.coldStorage.clear()
      } catch (error) {
        console.warn('Error clearing cold storage:', error)
      }
    }
    
    // Reset stats
    this.stats = {
      hits: 0,
      misses: 0,
      evictions: 0,
      size: 0,
      maxSize: this.hotCacheMaxSize,
      hotCacheSize: 0,
      warmCacheSize: 0,
      hotCacheHits: 0,
      hotCacheMisses: 0,
      warmCacheHits: 0,
      warmCacheMisses: 0
    }
  }
  
  /**
   * Get cache statistics
   * @returns Cache statistics
   */
  public getStats(): CacheStats {
    return { ...this.stats }
  }
  
  /**
   * Prefetch items based on ID patterns or relationships
   * @param ids Array of IDs to prefetch
   */
  public async prefetch(ids: string[]): Promise<void> {
    // Check if it's time to tune parameters
    await this.checkAndTuneParameters()
    
    // Prefetch in batches to avoid overwhelming the system
    const batches: string[][] = []
    
    // Split into batches using the configurable batch size
    for (let i = 0; i < ids.length; i += this.batchSize) {
      const batch = ids.slice(i, i + this.batchSize)
      batches.push(batch)
    }
    
    // Process each batch
    for (const batch of batches) {
      await Promise.all(
        batch.map(async (id) => {
          // Skip if already in hot cache
          if (this.hotCache.has(id)) return
          
          try {
            // Try to get from any cache level
            await this.get(id)
          } catch (error) {
            // Ignore errors during prefetching
            if (process.env.DEBUG) {
              console.warn(`Error prefetching ${id}:`, error)
            }
          }
        })
      )
    }
  }
  
  /**
   * Check if it's time to tune parameters and do so if needed
   * This is called before operations that might benefit from tuned parameters
   * 
   * This method serves as a checkpoint for auto-tuning, ensuring that:
   * 1. Parameters are tuned periodically based on the auto-tune interval
   * 2. Tuning happens before critical operations that would benefit from optimized parameters
   * 3. Tuning doesn't happen too frequently, which could impact performance
   * 
   * By calling this method before get(), getMany(), and prefetch() operations,
   * we ensure that the cache parameters are optimized for the current workload
   * without adding unnecessary overhead to every operation.
   */
  private async checkAndTuneParameters(): Promise<void> {
    // Skip if auto-tuning is disabled
    if (!this.autoTune) return
    
    // Check if it's time to tune parameters
    const now = Date.now()
    if (now - this.lastAutoTuneTime >= this.autoTuneInterval) {
      await this.tuneParameters()
    }
  }
  
  /**
   * Get multiple items at once, optimizing for batch retrieval
   * @param ids Array of IDs to get
   * @returns Map of ID to item
   */
  public async getMany(ids: string[]): Promise<Map<string, T>> {
    // Check if it's time to tune parameters
    await this.checkAndTuneParameters()
    
    const result = new Map<string, T>()
    
    // First check hot cache for all IDs
    const missingIds: string[] = []
    for (const id of ids) {
      const hotCacheEntry = this.hotCache.get(id)
      if (hotCacheEntry) {
        // Update access metadata
        hotCacheEntry.lastAccessed = Date.now()
        hotCacheEntry.accessCount++
        
        // Add to result
        result.set(id, hotCacheEntry.data)
        
        // Update stats
        this.stats.hits++
      } else {
        missingIds.push(id)
      }
    }
    
    if (missingIds.length === 0) {
      return result
    }
    
    // Try to get missing items from warm cache
    if (this.warmStorage) {
      try {
        const warmCacheItems = await this.warmStorage.getMany(missingIds)
        for (const [id, item] of warmCacheItems.entries()) {
          if (item) {
            // Promote to hot cache
            this.addToHotCache(id, item)
            
            // Add to result
            result.set(id, item)
            
            // Update stats
            this.stats.hits++
            
            // Remove from missing IDs
            const index = missingIds.indexOf(id)
            if (index !== -1) {
              missingIds.splice(index, 1)
            }
          }
        }
      } catch (error) {
        console.warn('Error accessing warm cache for batch:', error)
      }
    }
    
    if (missingIds.length === 0) {
      return result
    }
    
    // Try to get remaining missing items from cold storage
    if (this.coldStorage) {
      try {
        const coldStorageItems = await this.coldStorage.getMany(missingIds)
        for (const [id, item] of coldStorageItems.entries()) {
          if (item) {
            // Promote to hot and warm caches
            this.addToHotCache(id, item)
            await this.addToWarmCache(id, item)
            
            // Add to result
            result.set(id, item)
            
            // Update stats
            this.stats.misses++
          }
        }
      } catch (error) {
        console.warn('Error accessing cold storage for batch:', error)
      }
    }
    
    return result
  }
  
  /**
   * Set the storage adapters for warm and cold caches
   * @param warmStorage Warm cache storage adapter
   * @param coldStorage Cold storage adapter
   */
  public setStorageAdapters(warmStorage: any, coldStorage: any): void {
    this.warmStorage = warmStorage
    this.coldStorage = coldStorage
  }
}
