using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using MarketAlly.AIPlugin.Context.Configuration; using System.Collections.Concurrent; using System.Security.Cryptography; using System.Text; namespace MarketAlly.AIPlugin.Context.Performance { /// /// Manages caching for search results and frequently accessed context data /// public class CacheManager : IDisposable { private readonly MemoryCache _searchCache; private readonly MemoryCache _contextCache; private readonly ContextConfiguration _configuration; private readonly ILogger _logger; private readonly ConcurrentDictionary _cacheLocks; private readonly Timer _cleanupTimer; public CacheManager(ContextConfiguration configuration, ILogger logger) { _configuration = configuration; _logger = logger; _cacheLocks = new ConcurrentDictionary(); var cacheOptions = new MemoryCacheOptions { SizeLimit = _configuration.Performance.CacheSizeLimit, CompactionPercentage = _configuration.Performance.CacheCompactionPercentage }; _searchCache = new MemoryCache(cacheOptions); _contextCache = new MemoryCache(cacheOptions); // Setup cleanup timer to run every 10 minutes _cleanupTimer = new Timer(CleanupExpiredEntries, null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10)); _logger.LogDebug("Cache manager initialized with size limit: {SizeLimit}", cacheOptions.SizeLimit); } /// /// Gets cached search results for a query /// public async Task GetCachedSearchResultsAsync(string query, string projectPath, SearchParameters parameters) { if (!_configuration.Search.EnableCaching) return null; var cacheKey = GenerateSearchCacheKey(query, projectPath, parameters); if (_searchCache.TryGetValue(cacheKey, out SearchResults? results)) { _logger.LogDebug("Cache hit for search query: {Query}", query); return results; } _logger.LogDebug("Cache miss for search query: {Query}", query); return null; } /// /// Caches search results for future use /// public async Task CacheSearchResultsAsync(string query, string projectPath, SearchParameters parameters, SearchResults results) { if (!_configuration.Search.EnableCaching) return; var cacheKey = GenerateSearchCacheKey(query, projectPath, parameters); var lockKey = $"search_{cacheKey}"; var lockObject = _cacheLocks.GetOrAdd(lockKey, _ => new SemaphoreSlim(1, 1)); await lockObject.WaitAsync(); try { var cacheEntryOptions = new MemoryCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(_configuration.Search.CacheExpirationMinutes), Size = EstimateSearchResultsSize(results), Priority = CacheItemPriority.Normal }; _searchCache.Set(cacheKey, results, cacheEntryOptions); _logger.LogDebug("Cached search results for query: {Query}, expires in {Minutes} minutes", query, _configuration.Search.CacheExpirationMinutes); } finally { lockObject.Release(); } } /// /// Gets cached context entries for a file /// public async Task?> GetCachedContextEntriesAsync(string filePath) { var cacheKey = GenerateFileCacheKey(filePath); if (_contextCache.TryGetValue(cacheKey, out List? entries)) { var fileInfo = new System.IO.FileInfo(filePath); if (_contextCache.TryGetValue($"{cacheKey}_timestamp", out DateTime cachedTimestamp) && cachedTimestamp >= fileInfo.LastWriteTime) { _logger.LogDebug("Cache hit for context file: {FilePath}", filePath); return entries; } else { // File has been modified, remove from cache _contextCache.Remove(cacheKey); _contextCache.Remove($"{cacheKey}_timestamp"); } } _logger.LogDebug("Cache miss for context file: {FilePath}", filePath); return null; } /// /// Caches context entries for a file /// public async Task CacheContextEntriesAsync(string filePath, List entries) { var cacheKey = GenerateFileCacheKey(filePath); var lockKey = $"context_{cacheKey}"; var lockObject = _cacheLocks.GetOrAdd(lockKey, _ => new SemaphoreSlim(1, 1)); await lockObject.WaitAsync(); try { var cacheEntryOptions = new MemoryCacheEntryOptions { SlidingExpiration = TimeSpan.FromMinutes(30), // Keep in cache if accessed within 30 minutes Size = EstimateContextEntriesSize(entries), Priority = CacheItemPriority.High // Context entries are high priority }; _contextCache.Set(cacheKey, entries, cacheEntryOptions); _contextCache.Set($"{cacheKey}_timestamp", File.GetLastWriteTime(filePath), cacheEntryOptions); _logger.LogDebug("Cached {Count} context entries for file: {FilePath}", entries.Count, filePath); } finally { lockObject.Release(); } } /// /// Invalidates cache entries for a specific file /// public void InvalidateFileCache(string filePath) { var cacheKey = GenerateFileCacheKey(filePath); _contextCache.Remove(cacheKey); _contextCache.Remove($"{cacheKey}_timestamp"); _logger.LogDebug("Invalidated cache for file: {FilePath}", filePath); } /// /// Invalidates all search cache entries for a project /// public void InvalidateProjectSearchCache(string projectPath) { // Unfortunately, MemoryCache doesn't support wildcard removal // In a production system, you might want to use a more sophisticated cache like Redis _searchCache.Clear(); _logger.LogInformation("Cleared search cache for project changes"); } /// /// Gets cache statistics /// public CacheStatistics GetStatistics() { // Note: MemoryCache doesn't expose detailed statistics // In production, you might want to track these manually return new CacheStatistics { SearchCacheEnabled = _configuration.Search.EnableCaching, ContextCacheEnabled = true, CacheSizeLimit = _configuration.Performance.CacheSizeLimit, CompactionPercentage = _configuration.Performance.CacheCompactionPercentage }; } private string GenerateSearchCacheKey(string query, string projectPath, SearchParameters parameters) { var keyBuilder = new StringBuilder(); keyBuilder.Append($"search_{query}_{projectPath}"); keyBuilder.Append($"_{parameters.ContextType}_{parameters.Priority}"); keyBuilder.Append($"_{parameters.DaysBack}_{parameters.MaxResults}"); keyBuilder.Append($"_{parameters.IncludeContent}"); if (!string.IsNullOrEmpty(parameters.Tags)) { keyBuilder.Append($"_{parameters.Tags}"); } return GenerateHashKey(keyBuilder.ToString()); } private string GenerateFileCacheKey(string filePath) { return GenerateHashKey($"file_{filePath}"); } private string GenerateHashKey(string input) { using var sha256 = SHA256.Create(); var hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(input)); return Convert.ToBase64String(hashBytes).Replace("/", "_").Replace("+", "-").TrimEnd('='); } private long EstimateSearchResultsSize(SearchResults results) { // Rough estimation: each result ~1KB return results.Results.Count * 1024; } private long EstimateContextEntriesSize(List entries) { // Rough estimation based on content length return entries.Sum(e => e.Content.Length + e.Summary.Length + 500); // 500 bytes overhead per entry } private void CleanupExpiredEntries(object? state) { try { _searchCache.Compact(1.0); // Force cleanup of expired entries _contextCache.Compact(1.0); _logger.LogDebug("Completed cache cleanup cycle"); } catch (Exception ex) { _logger.LogError(ex, "Error during cache cleanup"); } } public void Dispose() { _cleanupTimer?.Dispose(); _searchCache?.Dispose(); _contextCache?.Dispose(); foreach (var lockObject in _cacheLocks.Values) { lockObject.Dispose(); } _cacheLocks.Clear(); } } /// /// Parameters for search operations used in cache key generation /// public class SearchParameters { public string ContextType { get; set; } = "all"; public string Priority { get; set; } = "all"; public int DaysBack { get; set; } = 0; public int MaxResults { get; set; } = 10; public bool IncludeContent { get; set; } = true; public string? Tags { get; set; } } /// /// Cache performance and configuration statistics /// public class CacheStatistics { public bool SearchCacheEnabled { get; set; } public bool ContextCacheEnabled { get; set; } public int CacheSizeLimit { get; set; } public double CompactionPercentage { get; set; } } }