import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/models/knowledge_base.dart'; import '../../../core/services/api_service.dart'; import '../../../core/services/cache_manager.dart'; import '../../../core/providers/app_providers.dart'; import '../../../core/utils/debug_logger.dart'; /// Cache keys for knowledge base data. const String _basesKey = 'knowledge_bases'; String _itemsKey(String baseId) => 'knowledge_items:$baseId'; /// TTL for knowledge cache entries. const Duration _knowledgeCacheTtl = Duration(minutes: 10); /// Centralized cache manager for knowledge base data. /// /// Uses the shared [CacheManager] pattern for TTL and LRU eviction. class KnowledgeCacheManager { static final KnowledgeCacheManager _instance = KnowledgeCacheManager._internal(); factory KnowledgeCacheManager() => _instance; KnowledgeCacheManager._internal(); final CacheManager _cache = CacheManager( defaultTtl: _knowledgeCacheTtl, maxEntries: 64, ); /// Returns cached knowledge bases, or null if not cached. List? getCachedBases() { final (hit: hit, value: bases) = _cache.lookup>(_basesKey); if (hit) { DebugLogger.log('cache-hit', scope: 'knowledge/bases'); } return hit ? bases : null; } /// Caches knowledge bases. void cacheBases(List bases) { _cache.write>(_basesKey, bases); DebugLogger.log( 'cache-write', scope: 'knowledge/bases', data: {'count': bases.length}, ); } /// Returns cached items for a knowledge base, or null if not cached. List? getCachedItems(String baseId) { final (hit: hit, value: items) = _cache.lookup>(_itemsKey(baseId)); if (hit) { DebugLogger.log('cache-hit', scope: 'knowledge/items', data: {'baseId': baseId}); } return hit ? items : null; } /// Caches items for a knowledge base. void cacheItems(String baseId, List items) { _cache.write>(_itemsKey(baseId), items); DebugLogger.log( 'cache-write', scope: 'knowledge/items', data: {'baseId': baseId, 'count': items.length}, ); } /// Clears all knowledge cache entries. void clear() { _cache.invalidateMatching((key) => key.startsWith('knowledge')); DebugLogger.log('cache-clear', scope: 'knowledge'); } /// Returns cache statistics for debugging. Map stats() => _cache.stats(); } /// State for the knowledge cache provider. class KnowledgeCacheState { const KnowledgeCacheState({ this.bases = const [], this.items = const >{}, this.isLoading = false, }); final List bases; final Map> items; final bool isLoading; KnowledgeCacheState copyWith({ List? bases, Map>? items, bool? isLoading, }) { return KnowledgeCacheState( bases: bases ?? this.bases, items: items ?? this.items, isLoading: isLoading ?? this.isLoading, ); } } /// Notifier that wraps [KnowledgeCacheManager] with Riverpod reactivity. class KnowledgeCacheNotifier extends Notifier { final _cacheManager = KnowledgeCacheManager(); @override KnowledgeCacheState build() { // Initialize from cache if available final cachedBases = _cacheManager.getCachedBases(); if (cachedBases != null && cachedBases.isNotEmpty) { return KnowledgeCacheState(bases: cachedBases); } return const KnowledgeCacheState(); } ApiService? get _api => ref.read(apiServiceProvider); Future ensureBases() async { // Check if already loaded in state if (state.bases.isNotEmpty) return; // Check cache final cached = _cacheManager.getCachedBases(); if (cached != null && cached.isNotEmpty) { state = state.copyWith(bases: cached); return; } if (_api == null) return; state = state.copyWith(isLoading: true); try { final bases = await _api!.getKnowledgeBases(); _cacheManager.cacheBases(bases); state = state.copyWith(bases: bases, isLoading: false); } catch (_) { state = state.copyWith(isLoading: false); } } Future fetchItemsForBase(String baseId) async { // Check if already in state if (state.items.containsKey(baseId)) return; // Check cache final cached = _cacheManager.getCachedItems(baseId); if (cached != null) { final next = Map>.from(state.items); next[baseId] = cached; state = state.copyWith(items: next); return; } if (_api == null) return; final next = Map>.from(state.items); try { final items = await _api!.getKnowledgeBaseItems(baseId); _cacheManager.cacheItems(baseId, items); next[baseId] = items; } catch (_) { next[baseId] = const []; } state = state.copyWith(items: next); } /// Clears both in-memory state and persistent cache. void clearCache() { _cacheManager.clear(); state = const KnowledgeCacheState(); } } final knowledgeCacheProvider = NotifierProvider( KnowledgeCacheNotifier.new, );