import 'dart:async'; import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../core/network/api_client.dart'; import '../../../core/network/signalr_service.dart'; import '../models/favorite_asset_model.dart'; class FavoritesState extends Equatable { final Set favoriteIsins; final List favoriteDetails; final bool isLoading; const FavoritesState({ this.favoriteIsins = const {}, this.favoriteDetails = const [], this.isLoading = false, }); bool isFavorite(String identifier) { if (identifier.isEmpty) return false; final upper = identifier.toUpperCase(); return favoriteIsins.contains(upper); } FavoritesState copyWith({ Set? favoriteIsins, List? favoriteDetails, bool? isLoading, }) { return FavoritesState( favoriteIsins: favoriteIsins ?? this.favoriteIsins, favoriteDetails: favoriteDetails ?? this.favoriteDetails, isLoading: isLoading ?? this.isLoading, ); } @override List get props => [favoriteIsins, favoriteDetails, isLoading]; } class FavoritesCubit extends Cubit { final ApiClient apiClient; final SignalRService? signalRService; StreamSubscription>? _priceSub; FavoritesCubit({required this.apiClient, this.signalRService}) : super(const FavoritesState()) { // 1. Subscribe to SignalR 10-second WebSocket price stream if (signalRService != null) { _priceSub = signalRService!.favoritePricesStream.listen((priceMap) { updatePrices(priceMap); }); } } @override Future close() { _priceSub?.cancel(); return super.close(); } /// Loads favorite metadata via REST ONLY WHEN NEEDED (e.g. initial load or list mutation). Future loadFavorites() async { emit(state.copyWith(isLoading: true)); try { final ts = DateTime.now().millisecondsSinceEpoch; final res = await apiClient.get('/api/v1/user/favorites?_t=$ts'); if (res.statusCode == 200 && res.data is List) { final rawList = res.data as List; final set = {}; final dedupMap = {}; for (var item in rawList) { final model = FavoriteAssetModel.fromJson(Map.from(item)); final key = (model.isin.isNotEmpty ? model.isin : (model.symbol.isNotEmpty ? model.symbol : model.name)).toUpperCase(); if (!dedupMap.containsKey(key)) { dedupMap[key] = model; } if (model.isin.isNotEmpty) set.add(model.isin.toUpperCase()); if (model.symbol.isNotEmpty) set.add(model.symbol.toUpperCase()); if (model.name.isNotEmpty) set.add(model.name.toUpperCase()); } emit(FavoritesState( favoriteIsins: set, favoriteDetails: dedupMap.values.toList(), isLoading: false, )); } else { emit(state.copyWith(isLoading: false)); } } catch (_) { emit(state.copyWith(isLoading: false)); } } /// Updates current prices and daily growth % live in-memory when SignalR WebSocket payload arrives. void updatePrices(Map priceMap) { if (priceMap.isEmpty) return; final updatedDetails = state.favoriteDetails.map((model) { final keyIsin = model.isin.toUpperCase(); final keySymbol = model.symbol.toUpperCase(); dynamic priceData = priceMap[keyIsin] ?? priceMap[keySymbol]; if (priceData != null && priceData is Map) { final double price = (priceData['currentPrice'] ?? priceData['price'] ?? model.currentPrice).toDouble(); final double change = (priceData['dailyChangePercent'] ?? priceData['change24h'] ?? model.change24h).toDouble(); return model.copyWith(currentPrice: price, change24h: change); } return model; }).toList(); emit(state.copyWith(favoriteDetails: updatedDetails)); } Future toggleFavorite(String identifier, {String? symbol, String? name}) async { if (identifier.isEmpty) return; final target = identifier.toUpperCase(); final isCurrentlyFav = state.isFavorite(target); // Optimistic UI update final newSet = Set.from(state.favoriteIsins); if (isCurrentlyFav) { newSet.remove(target); if (symbol != null) newSet.remove(symbol.toUpperCase()); if (name != null) newSet.remove(name.toUpperCase()); } else { newSet.add(target); if (symbol != null) newSet.add(symbol.toUpperCase()); if (name != null) newSet.add(name.toUpperCase()); } emit(state.copyWith(favoriteIsins: newSet)); // Perform API call in background try { if (isCurrentlyFav) { await apiClient.delete('/api/v1/user/favorites/$target'); } else { await apiClient.post('/api/v1/user/favorites/$target'); } // Re-sync full list to ensure metadata details are fresh await loadFavorites(); } catch (_) { // Revert on error await loadFavorites(); } } Future updateFavoriteTicker(String symbol, String ticker) async { try { // Optimistic UI update final target = symbol.toUpperCase(); final updatedDetails = state.favoriteDetails.map((model) { if (model.isin.toUpperCase() == target || model.symbol.toUpperCase() == target) { return model.copyWith(symbol: ticker); } return model; }).toList(); emit(state.copyWith(favoriteDetails: updatedDetails)); await apiClient.post('/api/v1/user/favorites/$symbol/ticker?ticker=$ticker'); await loadFavorites(); } catch (_) { // Revert/refresh on error await loadFavorites(); } } }