import 'dart:async'; import 'package:dio/dio.dart'; import 'package:hive_flutter/hive_flutter.dart'; import '../models/room.dart'; import '../models/message.dart'; import 'chat_config_loader.dart'; import 'chat_info_service.dart'; import 'package:geosector_app/core/constants/app_keys.dart'; /// Service de chat avec règles métier configurables via YAML /// Les permissions sont définies dans chat_config.yaml class ChatService { static ChatService? _instance; static ChatService get instance => _instance!; late final Dio _dio; late final Box _roomsBox; late final Box _messagesBox; late int _currentUserId; late String _currentUserName; late int _currentUserRole; late int? _currentUserEntite; String? _authToken; Timer? _syncTimer; /// Initialisation avec gestion des rôles et configuration YAML static Future init({ required String apiUrl, required int userId, required String userName, required int userRole, int? userEntite, String? authToken, }) async { _instance = ChatService._(); // Charger la configuration depuis le YAML await ChatConfigLoader.instance.loadConfig(); // Initialiser Hive await Hive.initFlutter(); Hive.registerAdapter(RoomAdapter()); Hive.registerAdapter(MessageAdapter()); // Ouvrir les boxes en utilisant les constantes centralisées _instance!._roomsBox = await Hive.openBox(AppKeys.chatRoomsBoxName); _instance!._messagesBox = await Hive.openBox(AppKeys.chatMessagesBoxName); // Configurer l'utilisateur _instance!._currentUserId = userId; _instance!._currentUserName = userName; _instance!._currentUserRole = userRole; _instance!._currentUserEntite = userEntite; _instance!._authToken = authToken; // Configurer Dio _instance!._dio = Dio(BaseOptions( baseUrl: apiUrl, connectTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 10), headers: authToken != null ? {'Authorization': 'Bearer $authToken'} : {}, )); // Démarrer la synchronisation _instance!._startSync(); } ChatService._(); /// Obtenir les destinataires possibles selon le rôle Future>> getPossibleRecipients({String? search}) async { try { // L'API utilise le token pour identifier l'utilisateur et son rôle String endpoint = '/chat/recipients'; final params = {}; if (search != null && search.isNotEmpty) { params['search'] = search; } final response = await _dio.get(endpoint, queryParameters: params); // Gérer différents formats de réponse if (response.data is List) { return List>.from(response.data); } else if (response.data is Map && response.data['recipients'] != null) { return List>.from(response.data['recipients']); } else if (response.data is Map && response.data['data'] != null) { return List>.from(response.data['data']); } else { print('⚠️ Format inattendu pour /chat/recipients: ${response.data.runtimeType}'); return []; } } catch (e) { print('⚠️ Erreur getPossibleRecipients: $e'); // Fallback sur logique locale selon le rôle return _getLocalRecipients(); } } /// Logique locale de récupération des destinataires List> _getLocalRecipients() { // Cette méthode devrait accéder à la box membres de l'app principale // Pour l'instant on retourne une liste vide return []; } /// Vérifier si l'utilisateur peut créer une conversation avec un destinataire bool canCreateConversationWith(int recipientRole, {int? recipientEntite}) { return ChatConfigLoader.instance.canSendMessageTo( senderRole: _currentUserRole, recipientRole: recipientRole, senderEntite: _currentUserEntite, recipientEntite: recipientEntite, ); } /// Obtenir les rooms filtrées selon les permissions Future> getRooms() async { try { // L'API filtre automatiquement selon le token Bearer final response = await _dio.get('/chat/rooms'); // Debug : afficher le type et le contenu de la réponse print('📊 Type de réponse /chat/rooms: ${response.data.runtimeType}'); if (response.data is Map) { print('📊 Clés de la réponse: ${(response.data as Map).keys.toList()}'); } // Gérer différents formats de réponse API List roomsData; if (response.data is Map) { // La plupart du temps, l'API retourne un objet avec status et rooms if (response.data['rooms'] != null) { roomsData = response.data['rooms'] as List; print('✅ Réponse API: status=${response.data['status']}, ${roomsData.length} rooms'); } else if (response.data['data'] != null) { roomsData = response.data['data'] as List; print('✅ Réponse avec propriété "data" (${roomsData.length} rooms)'); } else { // Pas de propriété rooms ou data, liste vide print('⚠️ Réponse sans rooms ni data: ${response.data}'); roomsData = []; } } else if (response.data is List) { // Si c'est directement une liste (moins courant) roomsData = response.data as List; print('✅ Réponse est directement une liste avec ${roomsData.length} rooms'); } else { // Format complètement inattendu print('⚠️ Format de réponse inattendu pour /chat/rooms: ${response.data.runtimeType}'); roomsData = []; } final rooms = roomsData .map((json) => Room.fromJson(json)) .toList(); // Sauvegarder dans Hive await _roomsBox.clear(); for (final room in rooms) { await _roomsBox.put(room.id, room); } // Mettre à jour les stats globales final totalUnread = rooms.fold(0, (sum, room) => sum + room.unreadCount); ChatInfoService.instance.updateStats( totalRooms: rooms.length, unreadMessages: totalUnread, ); return rooms; } catch (e) { print('Erreur lors de la récupération des rooms (utilisation du cache): $e'); // Fallback sur le cache local en cas d'erreur API (404, etc.) return _roomsBox.values.toList() ..sort((a, b) => (b.lastMessageAt ?? b.createdAt) .compareTo(a.lastMessageAt ?? a.createdAt)); } } /// Créer une room avec vérification des permissions Future createRoom({ required String title, required List participantIds, String? type, String? initialMessage, }) async { try { // Vérifier les permissions localement d'abord // L'API fera aussi une vérification final data = { 'title': title, 'type': type ?? (_currentUserRole == 9 ? 'broadcast' : 'private'), 'participants': participantIds, // L'API récupère le rôle et l'entité depuis le token }; // Ajouter le message initial s'il est fourni if (initialMessage != null && initialMessage.isNotEmpty) { data['initial_message'] = initialMessage; } final response = await _dio.post('/chat/rooms', data: data); final room = Room.fromJson(response.data); await _roomsBox.put(room.id, room); return room; } catch (e) { return null; } } /// Créer une conversation one-to-one Future createPrivateRoom({ required int recipientId, required String recipientName, required int recipientRole, int? recipientEntite, String? initialMessage, }) async { // Vérifier les permissions via la configuration if (!canCreateConversationWith(recipientRole, recipientEntite: recipientEntite)) { final messages = ChatConfigLoader.instance.getUIMessages(); throw Exception(messages['no_permission'] ?? 'Permission refusée'); } return createRoom( title: recipientName, participantIds: [recipientId], type: 'private', initialMessage: initialMessage, ); } /// Créer une conversation de groupe (pour superadmin) Future createGroupRoom({ required String title, required List adminIds, String? initialMessage, }) async { if (_currentUserRole != 9) { throw Exception('Seuls les superadmins peuvent créer des groupes'); } return createRoom( title: title, participantIds: adminIds, type: 'broadcast', initialMessage: initialMessage, ); } /// Obtenir les messages d'une room avec pagination Future> getMessages(String roomId, {String? beforeMessageId}) async { try { final params = { 'limit': 50, }; if (beforeMessageId != null) { params['before'] = beforeMessageId; } final response = await _dio.get('/chat/rooms/$roomId/messages', queryParameters: params); // Gérer différents formats de réponse List messagesData; bool hasMore = false; if (response.data is List) { // Si c'est directement une liste de messages messagesData = response.data as List; } else if (response.data is Map) { // Si c'est un objet avec messages et has_more messagesData = response.data['messages'] ?? response.data['data'] ?? []; hasMore = response.data['has_more'] ?? false; } else { print('⚠️ Format inattendu pour les messages: ${response.data.runtimeType}'); messagesData = []; } final messages = messagesData .map((json) => Message.fromJson(json, _currentUserId)) .toList(); // Sauvegarder dans Hive (en limitant à 100 messages par room) await _saveMessagesToCache(roomId, messages); return { 'messages': messages, 'has_more': hasMore, }; } catch (e) { print('Erreur getMessages: $e'); // Fallback sur le cache local final cachedMessages = _messagesBox.values .where((m) => m.roomId == roomId) .toList() ..sort((a, b) => a.sentAt.compareTo(b.sentAt)); return { 'messages': cachedMessages, 'has_more': false, }; } } /// Sauvegarder les messages dans le cache en limitant à 100 par room Future _saveMessagesToCache(String roomId, List newMessages) async { // Obtenir tous les messages existants pour cette room final existingMessages = _messagesBox.values .where((m) => m.roomId == roomId) .toList() ..sort((a, b) => b.sentAt.compareTo(a.sentAt)); // Plus récent en premier // Ajouter les nouveaux messages for (final message in newMessages) { await _messagesBox.put(message.id, message); } // Si on dépasse 100 messages, supprimer les plus anciens final allMessages = [...existingMessages, ...newMessages] ..sort((a, b) => b.sentAt.compareTo(a.sentAt)); if (allMessages.length > 100) { final messagesToDelete = allMessages.skip(100).toList(); for (final message in messagesToDelete) { await _messagesBox.delete(message.id); } } } /// Envoyer un message Future sendMessage(String roomId, String content) async { final tempId = 'temp_${DateTime.now().millisecondsSinceEpoch}'; final tempMessage = Message( id: tempId, roomId: roomId, content: content, senderId: _currentUserId, senderName: _currentUserName, sentAt: DateTime.now(), isMe: true, ); await _messagesBox.put(tempId, tempMessage); try { final response = await _dio.post( '/chat/rooms/$roomId/messages', data: { 'content': content, // L'API récupère le sender depuis le token }, ); final finalMessage = Message.fromJson(response.data, _currentUserId); await _messagesBox.delete(tempId); await _messagesBox.put(finalMessage.id, finalMessage); // Mettre à jour la room final room = _roomsBox.get(roomId); if (room != null) { final updatedRoom = Room( id: room.id, title: room.title, type: room.type, createdAt: room.createdAt, lastMessage: content, lastMessageAt: DateTime.now(), unreadCount: 0, ); await _roomsBox.put(roomId, updatedRoom); } return finalMessage; } catch (e) { return tempMessage; } } /// Marquer comme lu Future markAsRead(String roomId) async { try { await _dio.post('/chat/rooms/$roomId/read'); final room = _roomsBox.get(roomId); if (room != null) { // Décrémenter les messages non lus dans ChatInfoService if (room.unreadCount > 0) { ChatInfoService.instance.decrementUnread(room.unreadCount); } final updatedRoom = Room( id: room.id, title: room.title, type: room.type, createdAt: room.createdAt, lastMessage: room.lastMessage, lastMessageAt: room.lastMessageAt, unreadCount: 0, ); await _roomsBox.put(roomId, updatedRoom); } } catch (e) { // Ignorer } } /// Synchronisation périodique void _startSync() { _syncTimer?.cancel(); _syncTimer = Timer.periodic(const Duration(seconds: 30), (_) { getRooms(); }); } /// Nettoyer les ressources void dispose() { _syncTimer?.cancel(); _dio.close(); } // Getters Box get roomsBox => _roomsBox; Box get messagesBox => _messagesBox; int get currentUserId => _currentUserId; /// Obtenir le rôle de l'utilisateur actuel int getUserRole() => _currentUserRole; int get currentUserRole => _currentUserRole; String get currentUserName => _currentUserName; /// Obtenir le label du rôle String getRoleLabel(int role) { switch (role) { case 1: return 'Membre'; case 2: return 'Admin Amicale'; case 9: return 'Super Admin'; default: return 'Utilisateur'; } } /// Obtenir la description des permissions String getPermissionsDescription() { switch (_currentUserRole) { case 1: return 'Vous pouvez discuter avec les membres de votre amicale'; case 2: return 'Vous pouvez discuter avec les membres et les super admins'; case 9: return 'Vous pouvez envoyer des messages aux administrateurs d\'amicale'; default: return ''; } } }