import 'dart:async'; import 'package:votianlt_app/services/developer.dart' as developer; import '../app_state.dart'; import '../models/chat.dart'; import '../models/chat_message.dart'; import 'database_service.dart'; class ChatService { ChatService._internal(); static final ChatService _instance = ChatService._internal(); factory ChatService() => _instance; static const _jobIdPrefix = 'job:'; static const _jobNumberPrefix = 'job_number:'; static const _generalPrefix = 'general:'; static const _defaultGeneralConversationKey = 'general:allgemeine-nachrichten'; final DatabaseService _databaseService = DatabaseService(); final AppState _appState = AppState(); final List _chats = []; final StreamController> _chatsController = StreamController>.broadcast(); final StreamController _unreadCountController = StreamController.broadcast(); bool _initialized = false; Completer? _initCompleter; int _unreadCount = 0; Stream> get chatsStream => _chatsController.stream; List get currentChats => List.unmodifiable(_chats); Stream get unreadCountStream => _unreadCountController.stream; int get unreadCount => _unreadCount; Future initialize() async { if (_initialized) { await _loadChatsFromDatabase(); developer.log( 'ChatService already initialized, refreshed chats/unread count: $_unreadCount', name: 'ChatService', ); return; } if (_initCompleter != null) { return _initCompleter!.future; } _initCompleter = Completer(); developer.log('Initializing ChatService...', name: 'ChatService'); await _loadChatsFromDatabase(); _initialized = true; _initCompleter!.complete(); developer.log( 'ChatService initialized with unread count: $_unreadCount', name: 'ChatService', ); } Future dispose() async { await _chatsController.close(); await _unreadCountController.close(); _initialized = false; _initCompleter = null; } Future> loadMessagesForChat(String conversationKey) async { await initialize(); return _databaseService.loadChatMessages(conversationKey: conversationKey); } Future markConversationRead(String conversationKey) async { await initialize(); await _databaseService.markConversationRead(conversationKey); await _refreshConversation(conversationKey); // Note: _refreshConversation already calls _updateUnreadCount, // so we don't need to call it again here } Future deleteJobChats(String jobId, {String? jobNumber}) async { if (!_initialized) { await initialize(); } final trimmedJobId = jobId.trim(); final lowerJobId = trimmedJobId.toLowerCase(); final trimmedJobNumber = jobNumber?.trim() ?? ''; final lowerJobNumber = trimmedJobNumber.toLowerCase(); final conversationKeys = [ if (trimmedJobId.isNotEmpty) '$_jobIdPrefix$lowerJobId', if (trimmedJobNumber.isNotEmpty) '$_jobNumberPrefix$lowerJobNumber', ]; await _databaseService.deleteChatMessagesForJob( jobId: trimmedJobId.isNotEmpty ? trimmedJobId : null, jobNumber: trimmedJobNumber.isNotEmpty ? trimmedJobNumber : null, conversationKeys: conversationKeys, ); _chats.removeWhere((chat) { final matchesKey = conversationKeys.contains(chat.id); final matchesId = trimmedJobId.isNotEmpty && (chat.jobId?.trim().toLowerCase() == lowerJobId); final matchesNumber = trimmedJobNumber.isNotEmpty && (chat.jobNumber?.trim().toLowerCase() == lowerJobNumber); return matchesKey || matchesId || matchesNumber; }); _ensureDefaultGeneralChat(); _sortChats(); _emitChats(); await _updateUnreadCount(); developer.log( 'Removed chat conversations for jobId=$jobId jobNumber=$jobNumber', name: 'ChatService', ); } String conversationKeyForMessage(ChatMessage message) { developer.log( '[DEBUG_LOG] conversationKeyForMessage called for message ${message.id}, messageType=${message.messageType}, direction=${message.direction}', name: 'ChatService', ); // Messages with GENERAL messageType should always go to the default general chat if (message.messageType == ChatMessageType.general) { developer.log( '[DEBUG_LOG] GENERAL message detected, routing to conversation key: $_defaultGeneralConversationKey', name: 'ChatService', ); return _defaultGeneralConversationKey; } // Job-related messages go to job-specific chats final jobId = message.jobId?.trim(); if (jobId != null && jobId.isNotEmpty) { final normalizedJobId = jobId.toLowerCase(); final key = '$_jobIdPrefix$normalizedJobId'; developer.log( '[DEBUG_LOG] Job-related message (by jobId), routing to conversation key: $key', name: 'ChatService', ); return key; } final jobNumber = message.jobNumber?.trim(); if (jobNumber != null && jobNumber.isNotEmpty) { final normalizedJobNumber = jobNumber.toLowerCase(); final key = '$_jobNumberPrefix$normalizedJobNumber'; developer.log( '[DEBUG_LOG] Job-related message (by jobNumber), routing to conversation key: $key', name: 'ChatService', ); return key; } developer.log( '[DEBUG_LOG] No job context available, routing to default general chat', name: 'ChatService', ); return _defaultGeneralConversationKey; } Future saveIncomingMessage(ChatMessage message) async { if (!_initialized) { await initialize(); } await _persistMessage(message.copyWith(pendingSync: false)); } Future saveOutgoingMessage(ChatMessage message) async { if (!_initialized) { await initialize(); } await _persistMessage(message); } Future markOutgoingMessageSynced(String messageId) async { if (!_initialized) { await initialize(); } final conversationKey = await _databaseService.updateChatMessagePendingSync( messageId, false, ); if (conversationKey != null && conversationKey.isNotEmpty) { await _refreshConversation(conversationKey); } } Future _persistMessage(ChatMessage message) async { final conversationKey = conversationKeyForMessage(message); final jobId = message.jobId?.trim(); if (jobId != null && jobId.isNotEmpty) { final legacyKey = '$_jobIdPrefix$jobId'; if (legacyKey != conversationKey) { await _databaseService.migrateConversationKey( legacyKey, conversationKey, ); } } else { final jobNumber = message.jobNumber?.trim(); if (jobNumber != null && jobNumber.isNotEmpty) { final legacyKey = '$_jobNumberPrefix$jobNumber'; if (legacyKey != conversationKey) { await _databaseService.migrateConversationKey( legacyKey, conversationKey, ); } } } await _databaseService.upsertChatMessage(message, conversationKey); if (!message.pendingSync) { await _databaseService.removePendingDuplicates(conversationKey, message); } await _refreshConversation(conversationKey); } Future _loadChatsFromDatabase() async { await _databaseService.ensureInitialized(); final grouped = await _loadNormalizedChatGroups(); _chats.clear(); grouped.forEach((conversationKey, messages) { final chat = _buildChat(conversationKey, messages); if (chat != null) { _chats.add(chat); } }); _ensureDefaultGeneralChat(); _sortChats(); _emitChats(); await _updateUnreadCount(); } Future _refreshConversation(String conversationKey) async { if (_isLegacyGeneralConversationKey(conversationKey)) { await _databaseService.migrateConversationKey( conversationKey, _defaultGeneralConversationKey, ); conversationKey = _defaultGeneralConversationKey; } final messages = await _databaseService.loadChatMessages( conversationKey: conversationKey, ); final index = _chats.indexWhere((chat) => chat.id == conversationKey); if (messages.isEmpty) { if (index != -1) { _chats.removeAt(index); } _ensureDefaultGeneralChat(); _sortChats(); _emitChats(); await _updateUnreadCount(); return; } final chat = _buildChat(conversationKey, messages); if (chat == null) { return; } if (index == -1) { _chats.add(chat); } else { _chats[index] = chat; } _ensureDefaultGeneralChat(); _sortChats(); _emitChats(); await _updateUnreadCount(); } Chat? _buildChat(String conversationKey, List messages) { if (messages.isEmpty) { return null; } messages.sort((a, b) => a.createdAt.compareTo(b.createdAt)); final lastMessage = messages.last; final jobId = messages .map((m) => m.jobId) .firstWhere( (value) => value != null && value.isNotEmpty, orElse: () => null, ); final jobNumber = messages .map((m) => m.jobNumber) .firstWhere( (value) => value != null && value.isNotEmpty, orElse: () => null, ); final counterpart = _determineCounterpart(conversationKey, messages); final isJobChat = conversationKey.startsWith(_jobIdPrefix) || conversationKey.startsWith(_jobNumberPrefix) || messages.any((m) => m.messageType == ChatMessageType.jobRelated); final chatType = isJobChat ? ChatType.jobSpecific : ChatType.general; final counterpartNormalized = counterpart != null && counterpart.toLowerCase() == _appState.loggedInEmail!.toLowerCase() ? _appState.loggedInEmail! : counterpart; final bool isDefaultGeneral = !isJobChat && conversationKey == _defaultGeneralConversationKey; final title = isJobChat ? _buildJobTitle(jobNumber, jobId) : (isDefaultGeneral ? 'Allgemeine Nachrichten' : (counterpart ?? 'Allgemeiner Chat')); return Chat( id: conversationKey, title: title, receiver: counterpartNormalized, type: chatType, jobId: jobId, jobNumber: jobNumber, messages: List.unmodifiable(messages), lastMessageTime: lastMessage.createdAt, lastMessagePreview: lastMessage.contentType == ChatContentType.image ? '[Bild]' : lastMessage.content, ); } String _buildJobTitle(String? jobNumber, String? jobId) { if (jobNumber != null && jobNumber.isNotEmpty) { return 'Job $jobNumber'; } if (jobId != null && jobId.length >= 6) { return 'Job ${jobId.substring(0, 6).toUpperCase()}'; } return 'Job-Chat'; } String? _determineCounterpart( String conversationKey, List messages, ) { // Receiver is always the userId for general chats return _appState.loggedInEmail; } void _sortChats() { _chats.sort((a, b) => b.lastMessageTime.compareTo(a.lastMessageTime)); } void _emitChats() { if (_chatsController.isClosed) { return; } _chatsController.add(List.unmodifiable(_chats)); } Future _updateUnreadCount() async { try { await _databaseService.ensureInitialized(); final count = await _databaseService.getTotalUnreadMessageCount(); developer.log( '[DEBUG_LOG] Unread count from database: $count', name: 'ChatService', ); _unreadCount = count; if (!_unreadCountController.isClosed) { _unreadCountController.add(count); developer.log( '[DEBUG_LOG] Emitted unread count to stream: $count', name: 'ChatService', ); } else { developer.log( '[DEBUG_LOG] Unread count controller is closed, cannot emit', name: 'ChatService', ); } } catch (e, st) { developer.log('Error updating unread count: $e', name: 'ChatService'); developer.log('Stack trace: $st', name: 'ChatService'); } } Future>> _loadNormalizedChatGroups() async { var grouped = await _databaseService.loadAllChatMessagesGrouped(); final legacyGeneralKeys = grouped.keys.where(_isLegacyGeneralConversationKey).toList(); if (legacyGeneralKeys.isEmpty) { return grouped; } for (final key in legacyGeneralKeys) { await _databaseService.migrateConversationKey( key, _defaultGeneralConversationKey, ); } grouped = await _databaseService.loadAllChatMessagesGrouped(); return grouped; } bool _isLegacyGeneralConversationKey(String conversationKey) { return conversationKey != _defaultGeneralConversationKey && conversationKey.startsWith(_generalPrefix) && !conversationKey.startsWith(_jobIdPrefix) && !conversationKey.startsWith(_jobNumberPrefix); } void _ensureDefaultGeneralChat() { final receiver = _appState.loggedInEmail; if (receiver == null || receiver.isEmpty) { developer.log( '[DEBUG_LOG] _ensureDefaultGeneralChat: No receiver available, skipping', name: 'ChatService', ); return; } const conversationKey = _defaultGeneralConversationKey; developer.log( '[DEBUG_LOG] _ensureDefaultGeneralChat: Creating/ensuring default general chat with key: $conversationKey (receiver=$receiver)', name: 'ChatService', ); _chats.removeWhere( (chat) => chat.id != conversationKey && chat.type == ChatType.general && chat.receiver != null && chat.receiver!.toLowerCase() == receiver.toLowerCase() && chat.messages.isEmpty, ); final index = _chats.indexWhere((chat) => chat.id == conversationKey); if (index == -1) { developer.log( '[DEBUG_LOG] _ensureDefaultGeneralChat: Chat not found, creating new "Allgemeine Nachrichten" chat', name: 'ChatService', ); _chats.add( Chat( id: conversationKey, title: 'Allgemeine Nachrichten', receiver: receiver, type: ChatType.general, jobId: null, jobNumber: null, messages: const [], lastMessageTime: DateTime.fromMillisecondsSinceEpoch(0), lastMessagePreview: 'Noch keine Nachrichten', ), ); } else { developer.log( '[DEBUG_LOG] _ensureDefaultGeneralChat: Chat already exists at index $index, verifying/updating it', name: 'ChatService', ); final existing = _chats[index]; if (existing.type != ChatType.general || existing.receiver == null || existing.receiver!.toLowerCase() != receiver.toLowerCase() || (existing.messages.isEmpty && existing.title != 'Allgemeine Nachrichten')) { developer.log( '[DEBUG_LOG] _ensureDefaultGeneralChat: Updating existing chat to ensure correct settings', name: 'ChatService', ); _chats[index] = Chat( id: existing.id, title: existing.messages.isEmpty ? 'Allgemeine Nachrichten' : existing.title, receiver: receiver, type: ChatType.general, jobId: existing.jobId, jobNumber: existing.jobNumber, messages: existing.messages, lastMessageTime: existing.lastMessageTime, lastMessagePreview: existing.lastMessagePreview, ); } else { developer.log( '[DEBUG_LOG] _ensureDefaultGeneralChat: Existing chat is already correctly configured (${existing.messages.length} messages)', name: 'ChatService', ); } } } }