import 'dart:async'; import '../../core/errors/failure.dart'; import '../../core/utils/result.dart'; import '../models/content_provider_config.dart'; import '../models/post.dart'; import '../models/post_comment.dart'; import '../models/provider_diagnostics.dart'; import '../models/provider_health.dart'; import '../models/tag_suggestion.dart'; import '../models/top_period_filter.dart'; import '../repositories/provider_repository.dart'; import 'content_provider.dart'; import 'provider_factory.dart'; class ProviderManager { ProviderManager(this._repository, this._factory); final ProviderRepository _repository; final ProviderFactory _factory; Future>> loadConfigs({ bool enabledOnly = true, }) async { await _repository.ensureSeedProviders(); return _repository.getProviders(enabledOnly: enabledOnly); } Future>> activeProviders() async { final result = await loadConfigs(); if (result is Error>) { return Error(result.failure); } final configs = (result as Success>).data ..sort((a, b) => a.priority.compareTo(b.priority)); return Success(configs.map(_factory.create).toList()); } Future> enableProvider(String id, bool enabled) async { final result = await _repository.getProvider(id); return result.fold( onSuccess: (config) { if (config == null) { return const Error( Failure(code: 'not_found', message: 'Provider not found'), ); } return _repository.saveProvider( config.copyWith(enabled: enabled, updatedAt: DateTime.now()), ); }, onError: Error.new, ); } Future> addCustomProvider(ContentProviderConfig config) { return _repository.saveProvider(config.copyWith(updatedAt: DateTime.now())); } Future> deleteProvider(String id) => _repository.deleteProvider(id); Future>> checkAll({int concurrency = 3}) async { final providersResult = await activeProviders(); if (providersResult is Error>) { return Error(providersResult.failure); } final providers = (providersResult as Success>).data; if (providers.isEmpty) return const Success([]); final results = await _runLimited( providers, concurrency, (provider) async { final health = await provider.checkHealth(); await _repository.saveHealth(health); return health; }, ); return Success(results); } Future>> searchAcrossProviders({ required List tags, required int page, int limit = 50, String? rating, String? providerId, TopPeriodFilter topPeriod = TopPeriodFilter.none, }) async { final providersResult = await activeProviders(); if (providersResult is Error>) { return Error(providersResult.failure); } var providers = (providersResult as Success>).data; if (providerId != null) { providers = providers.where((provider) => provider.id == providerId).toList(); } final posts = []; final seen = {}; for (final provider in providers) { try { final providerPosts = await provider.searchPosts( tags: tags, page: page, limit: limit, rating: rating, topPeriod: topPeriod, ); for (final post in providerPosts) { if (seen.add(post.cacheKey)) posts.add(post); } await _repository.saveDiagnostics( ProviderDiagnostics( providerId: provider.id, lastSearchAt: DateTime.now(), lastResultCount: providerPosts.length, ), ); } catch (_) { await _repository.saveDiagnostics( ProviderDiagnostics( providerId: provider.id, lastSearchAt: DateTime.now(), lastResultCount: 0, lastErrorMessage: 'Search failed', ), ); await _repository.saveHealth( ProviderHealth( providerId: provider.id, status: ProviderStatus.offline, pingMs: 0, lastCheckedAt: DateTime.now(), errorMessage: 'Search failed', ), ); } } return Success(posts); } Future>> getComments( String providerId, String postId, ) async { final providersResult = await activeProviders(); if (providersResult is Error>) { return Error(providersResult.failure); } final providers = (providersResult as Success>).data; final matches = providers.where((provider) => provider.id == providerId); if (matches.isEmpty) { return const Error( Failure(code: 'not_found', message: 'Provider not found'), ); } final provider = matches.first; if (provider is! CommentProvider) return const Success([]); try { return Success(await (provider as CommentProvider).getComments(postId)); } catch (error) { return Error( Failure( code: 'comments_unavailable', message: 'Comments unavailable', details: error, ), ); } } Future> getPost(String providerId, String postId) async { final providersResult = await activeProviders(); if (providersResult is Error>) { return Error(providersResult.failure); } final providers = (providersResult as Success>).data; final matches = providers.where((provider) => provider.id == providerId); if (matches.isEmpty) { return const Error( Failure(code: 'not_found', message: 'Provider not found')); } try { return Success(await matches.first.getPost(postId)); } catch (error) { return Error( Failure( code: 'provider_get_post', message: 'Could not load post', details: error, ), ); } } Future>> suggestTags( String query, { int limit = 20, }) async { final providersResult = await activeProviders(); if (providersResult is Error>) { return Error(providersResult.failure); } final providers = (providersResult as Success>) .data .whereType() .toList(); final suggestions = {}; for (final provider in providers) { try { final items = await provider.suggestTags(query, limit: limit); for (final item in items) { suggestions.putIfAbsent( '${item.providerId}:${item.name}', () => item); } } catch (_) { // Suggestions are non-critical; a failed provider should not affect UI. } if (suggestions.length >= limit) break; } final values = suggestions.values.toList() ..sort((a, b) => b.postCount.compareTo(a.postCount)); return Success(values.take(limit).toList()); } Future> _runLimited( List items, int concurrency, Future Function(T item) action, ) async { final results = []; var index = 0; Future worker() async { while (index < items.length) { final current = items[index++]; results.add(await action(current)); } } await Future.wait( List.generate(concurrency.clamp(1, items.length), (_) => worker()), ); return results; } }