import 'dart:async'; import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:votianlt_app/services/developer.dart' as developer; import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:web_socket_channel/status.dart' as ws_status; import 'dart:math'; import 'database_service.dart'; import 'chat_service.dart'; import 'notification_service.dart'; import 'location_service.dart'; import '../app_state.dart'; import '../models/chat_message.dart'; import '../models/job.dart'; import '../models/queued_message.dart'; import 'dart_mq.dart'; class WebSocketService { static final WebSocketService _instance = WebSocketService._internal(); factory WebSocketService() => _instance; WebSocketService._internal(); WebSocketChannel? _wsChannel; StreamSubscription? _wsSubscription; bool _isConnected = false; bool _isConnecting = false; // Authentication state bool _isAuthenticated = false; String? _authToken; // Keep last known values for UI initialization (Behavior-like) Map? _lastAuthResponse; // Completer to await a graceful WebSocket disconnect Completer? _disconnectCompleter; // Unique persistent App ID for client identification String? _appId; // Automatic reconnection timer Timer? _reconnectTimer; // In-memory message buffer for messages sent while disconnected final List<_BufferedMessage> _messageBuffer = []; // Database service final DatabaseService _databaseService = DatabaseService(); // Validator for optional jobId field on chat messages final RegExp _jobIdRegExp = RegExp(r'^[0-9a-fA-F]{24}$'); /// Ensure a unique persistent App ID exists. /// Generates a UUID v4 if not already stored. Future _ensureAppId() async { if (_appId != null) return; try { final existing = await _databaseService.loadKeyValue('appId'); if (existing != null && existing.isNotEmpty) { _appId = existing; developer.log( 'Loaded existing appId: $_appId', name: 'WebSocketService', ); return; } } catch (_) {} // Generate a UUID v4 and persist _appId = _generateUuid(); developer.log('Generated new appId: $_appId', name: 'WebSocketService'); try { await _databaseService.saveKeyValue('appId', _appId!); } catch (_) {} } /// Get the unique persistent App ID (for external access if needed) String? get appId => _appId; /// Generate a UUID v4 String _generateUuid() { final rand = Random(); final buf = StringBuffer(); for (int i = 0; i < 36; i++) { if (i == 8 || i == 13 || i == 18 || i == 23) { buf.write('-'); } else if (i == 14) { buf.write('4'); } else if (i == 19) { buf.write(['8', '9', 'a', 'b'][rand.nextInt(4)]); } else { buf.write(rand.nextInt(16).toRadixString(16)); } } return buf.toString(); } // --------------------------------------------------------------------------- // WebSocket Connection // --------------------------------------------------------------------------- /// Build the WebSocket URL /// Im Release-Modus: votianlt.de (Produktionsserver) /// Im Debug-Modus: localhost (Android Emulator: 10.0.2.2) String _buildWebSocketUrl() { // Release-Modus: Verbindung zum Produktionsserver if (kReleaseMode) { return 'wss://votianlt.de/ws/messaging'; } return 'ws://192.168.180.10:8082/ws/messaging'; } /// Handle a connected WebSocket (common setup for connect and reconnect) void _onWebSocketConnected() { developer.log('WebSocket connected', name: 'WebSocketService'); // Update internal connection state _isConnected = true; _isConnecting = false; // Note: Don't publish connectionStatus=true yet - wait for successful auth // Re-run the same setup as initial connection (auto-login) _setupAfterConnect(); } /// Setup auto-login after connection (initial or reconnect) Future _setupAfterConnect() async { try { // Ensure we have an appId await _ensureAppId(); // Auto-login with saved credentials if user was previously logged in final credentials = await _databaseService.loadCredentials(); if (credentials != null) { developer.log( 'Auto-login with saved credentials for ${credentials.email}', name: 'WebSocketService', ); await login(credentials.email, credentials.password); } } catch (e, st) { developer.log( 'Error in _setupAfterConnect: $e', name: 'WebSocketService', ); developer.log('Stack: $st', name: 'WebSocketService'); } } /// Handle WebSocket disconnection void _handleWebSocketDisconnect() { developer.log('WebSocket disconnected', name: 'WebSocketService'); _isConnected = false; _isAuthenticated = false; Future.microtask(() { DartMQ().publish(MQTopics.connectionStatus, false); }); // Clean up WebSocket resources _wsSubscription?.cancel(); _wsSubscription = null; _wsChannel = null; try { _disconnectCompleter?.complete(); } catch (_) {} _disconnectCompleter = null; // Start automatic reconnection attempts _startReconnectTimer(); } void _startReconnectTimer() { _stopReconnectTimer(); _reconnectTimer = Timer.periodic(const Duration(seconds: 15), (timer) { if (_isConnected || _isConnecting) { _stopReconnectTimer(); return; } developer.log( 'Attempting automatic reconnection...', name: 'WebSocketService', ); connect(); }); } void _stopReconnectTimer() { _reconnectTimer?.cancel(); _reconnectTimer = null; } /// Force a clean reconnect after the app resumes from standby. /// Keeps buffered outbound messages intact and relies on saved credentials /// for the subsequent auto-login inside [connect]. Future reconnectForAppResume() async { final credentials = await _databaseService.loadCredentials(); if (credentials == null) { developer.log( 'Skipping reconnect after resume - no saved credentials', name: 'WebSocketService', ); return; } if (_isConnecting) { developer.log( 'Skipping reconnect after resume - connection attempt already running', name: 'WebSocketService', ); return; } developer.log( 'Restarting WebSocket connection after app resume', name: 'WebSocketService', ); _stopReconnectTimer(); final existingSubscription = _wsSubscription; final existingChannel = _wsChannel; _wsSubscription = null; _wsChannel = null; _disconnectCompleter = null; _isConnected = false; _isConnecting = false; _isAuthenticated = false; _authToken = null; _lastAuthResponse = null; Future.microtask(() { DartMQ().publish(MQTopics.connectionStatus, false); }); try { await existingSubscription?.cancel(); } catch (e, st) { developer.log( 'Error cancelling old WebSocket subscription on resume: $e', name: 'WebSocketService', ); developer.log('Stack: $st', name: 'WebSocketService'); } try { await existingChannel?.sink.close(ws_status.goingAway); } catch (e, st) { developer.log( 'Error closing old WebSocket channel on resume: $e', name: 'WebSocketService', ); developer.log('Stack: $st', name: 'WebSocketService'); } await connect(); } // --------------------------------------------------------------------------- // WebSocket Send / Receive // --------------------------------------------------------------------------- /// Send a message over WebSocket in wire format: {"topic": ..., "payload": ...} bool _sendWebSocket(String topic, String jsonPayload) { if (!_isConnected || _wsChannel == null) { developer.log( 'Cannot send, not connected. Topic=$topic', name: 'WebSocketService', ); return false; } try { final parsed = jsonDecode(jsonPayload); final wireMessage = jsonEncode({'topic': topic, 'payload': parsed}); developer.log('>> SEND: $wireMessage', name: 'WebSocketService'); _wsChannel!.sink.add(wireMessage); return true; } catch (e, st) { developer.log( 'Error sending WebSocket message: $e', name: 'WebSocketService', ); developer.log('Stack: $st', name: 'WebSocketService'); return false; } } /// Handle incoming WebSocket message void _onWebSocketMessage(dynamic rawData) async { if (rawData is! String) { developer.log( 'Received non-text WebSocket message, ignoring', name: 'WebSocketService', ); return; } try { final wireMessage = jsonDecode(rawData) as Map; final topic = wireMessage['topic'] as String?; final payload = wireMessage['payload']; if (topic == null) { developer.log( 'WebSocket message missing topic field', name: 'WebSocketService', ); return; } developer.log('<< RECEIVED: $rawData', name: 'WebSocketService'); await _handleMessage(topic, payload); } catch (e, st) { developer.log( 'Error parsing WebSocket message: $e', name: 'WebSocketService', ); developer.log('Stack: $st', name: 'WebSocketService'); } } // --------------------------------------------------------------------------- // Message Handlers // --------------------------------------------------------------------------- Future _handleMessage(String topic, dynamic data) async { developer.log( '_handleMessage called with topic: $topic', name: 'WebSocketService', ); if (topic.startsWith('/client/')) { await _handleClientMessage(topic, data); } else { developer.log( 'Topic does not start with /client/, ignoring', name: 'WebSocketService', ); } } Future _handleClientMessage(String topic, dynamic data) async { developer.log( 'Handling client message: topic=$topic, dataType=${data.runtimeType}', name: 'WebSocketService', ); if (topic.endsWith('/auth')) { await _handleAuthMessage(topic, data); } else if (topic.endsWith('/jobs')) { _handleJobsMessage(data); } else if (topic.endsWith('/job_deleted')) { _handleJobDeletedMessage(data); } else if (topic.endsWith('/job_created')) { _handleJobCreatedMessage(data); } else if (topic.endsWith('/message_ack')) { await _handleChatMessageAck(data); } else if (topic.endsWith('/message')) { await _handleChatMessage(topic, data); } else { _handleOtherClientMessage(topic, data); } } Future _handleAuthMessage( String topic, Map data, ) async { _lastAuthResponse = data; DartMQ().publish>(MQTopics.authResponse, data); if (data['success'] == true) { await _handleSuccessfulAuth(); } else { _handleFailedAuth(); } } Future _handleSuccessfulAuth() async { _isAuthenticated = true; developer.log('Auth successful', name: 'WebSocketService'); // Publish connection status to UI - fully connected and authenticated (async to avoid build-phase issues) Future.microtask(() { DartMQ().publish(MQTopics.connectionStatus, true); }); // Flush any messages that were buffered while disconnected. // This also clears local jobs and notifies the server. await _flushMessageBuffer(); // Start location tracking only if enabled in auth response final locationTrackingEnabled = _lastAuthResponse?['locationTrackingEnabled'] == true; developer.log( 'Location tracking enabled: $locationTrackingEnabled', name: 'WebSocketService', ); if (locationTrackingEnabled) { LocationService().startTracking(); developer.log('Location tracking started', name: 'WebSocketService'); } else { developer.log( 'Location tracking disabled by server', name: 'WebSocketService', ); } } void _handleFailedAuth() { _isAuthenticated = false; _authToken = null; } /// Übersetzung deaktiviert - Texte werden im Original angezeigt Future> _translateJobData( Map jobData, ) async { // Keine Übersetzung - Daten werden so wie vom Server empfangen verwendet return jobData; } void _handleJobsMessage(List data) async { final jobs = data; // Log empfangene Jobs JSON developer.log( '<< JOBS RECEIVED: ${jsonEncode(data)}', name: 'WebSocketService', ); if (jobs.isNotEmpty) { final currentJobCount = AppState().assignedJobs.length; if (currentJobCount > 0 && jobs.length > currentJobCount) { final newCount = jobs.length - currentJobCount; NotificationService().showJobNotification( title: 'Neue Jobs', body: newCount == 1 ? 'Sie haben einen neuen Job erhalten.' : 'Sie haben $newCount neue Jobs erhalten.', ); } // Parse and persist jobs to database immediately try { final List parsedJobs = []; for (final jobData in jobs) { try { Map actualJobData; if (jobData is Map && jobData.containsKey('job')) { actualJobData = Map.from( jobData['job'] as Map, ); if (jobData.containsKey('tasks') && jobData['tasks'] is List) { actualJobData['tasks'] = jobData['tasks']; } if (jobData.containsKey('cargoItems') && jobData['cargoItems'] is List) { actualJobData['cargoItems'] = jobData['cargoItems']; } } else { actualJobData = jobData as Map; } // Übersetze Textfelder vor dem Speichern actualJobData = await _translateJobData(actualJobData); final job = Job.fromJson(actualJobData); parsedJobs.add(job); } catch (e, stackTrace) { developer.log('Error parsing job: $e', name: 'WebSocketService'); developer.log('Stack trace: $stackTrace', name: 'WebSocketService'); } } // Save all parsed jobs to database if (parsedJobs.isNotEmpty) { await _databaseService.saveJobs(parsedJobs); developer.log( 'Saved ${parsedJobs.length} jobs to database', name: 'WebSocketService', ); } } catch (e, stackTrace) { developer.log( 'Error saving jobs to database: $e', name: 'WebSocketService', ); developer.log('Stack trace: $stackTrace', name: 'WebSocketService'); } DartMQ().publish>(MQTopics.jobsResponse, jobs); } else { // Clear all jobs from database when empty list received await _databaseService.clearAllJobsAndRelatedData(); developer.log( 'Cleared all jobs from database (empty list received)', name: 'WebSocketService', ); // Still publish empty list to complete any waiting operations DartMQ().publish>(MQTopics.jobsResponse, []); } } void _handleJobDeletedMessage(Map data) async { final jobId = data['jobId']?.toString(); final jobNumber = data['jobNumber']?.toString(); if (jobId == null || jobId.isEmpty) { developer.log( 'Received job_deleted message without jobId', name: 'WebSocketService', ); return; } developer.log( '<< JOB DELETED: jobId=$jobId, jobNumber=$jobNumber', name: 'WebSocketService', ); // Delete job from database immediately try { await _databaseService.deleteJob(jobId); developer.log( 'Deleted job $jobId from database', name: 'WebSocketService', ); } catch (e, stackTrace) { developer.log( 'Error deleting job $jobId from database: $e', name: 'WebSocketService', ); developer.log('Stack trace: $stackTrace', name: 'WebSocketService'); } // Publish event via DartMQ for UI to handle DartMQ().publish>(MQTopics.jobDeleted, data); } void _handleJobCreatedMessage(Map data) async { final jobId = data['job']?['id']?.toString() ?? data['id']?.toString(); final jobNumber = data['job']?['jobNumber']?.toString() ?? data['jobNumber']?.toString(); // Log empfangenes Job JSON developer.log( '<< JOB CREATED JSON: ${jsonEncode(data)}', name: 'WebSocketService', ); developer.log( '<< JOB CREATED: jobId=$jobId, jobNumber=$jobNumber', name: 'WebSocketService', ); // Parse and persist job to database immediately try { Map actualJobData; if (data.containsKey('job')) { actualJobData = Map.from( data['job'] as Map, ); if (data.containsKey('tasks') && data['tasks'] is List) { actualJobData['tasks'] = data['tasks']; } if (data.containsKey('cargoItems') && data['cargoItems'] is List) { actualJobData['cargoItems'] = data['cargoItems']; } } else { actualJobData = data; } // Übersetze Textfelder vor dem Speichern actualJobData = await _translateJobData(actualJobData); final job = Job.fromJson(actualJobData); await _databaseService.saveOrUpdateJob(job); developer.log( 'Saved new job ${job.id} to database', name: 'WebSocketService', ); } catch (e, stackTrace) { developer.log( 'Error saving new job to database: $e', name: 'WebSocketService', ); developer.log('Stack trace: $stackTrace', name: 'WebSocketService'); } // Show notification with sound for new job NotificationService().showJobNotification( title: 'Neuer Job', body: jobNumber != null && jobNumber.isNotEmpty ? 'Job $jobNumber wurde Ihnen zugewiesen.' : 'Sie haben einen neuen Job erhalten.', ); // Publish event via DartMQ for UI to handle DartMQ().publish>(MQTopics.jobCreated, data); } Future _handleChatMessage( String topic, Map data, ) async { const requiredFields = [ 'messageId', 'content', 'origin', 'messageType', 'createdAt', ]; final missing = []; for (final field in requiredFields) { final value = data[field]; if (value == null || (value is String && value.trim().isEmpty)) { missing.add(field); } } if (missing.isNotEmpty) { return; } try { final message = ChatMessage.fromJson(data); await ChatService().saveIncomingMessage(message); final conversationKey = ChatService().conversationKeyForMessage(message); String notificationTitle; if (message.messageType == ChatMessageType.jobRelated) { final jobNumber = message.jobNumber ?? ''; notificationTitle = jobNumber.isNotEmpty ? 'Nachricht zu Job $jobNumber' : 'Neue Job-Nachricht'; } else { notificationTitle = 'Neue Nachricht'; } String notificationBody; if (message.contentType == ChatContentType.image) { notificationBody = '[Bild]'; } else { final content = message.content; notificationBody = content.length > 100 ? '${content.substring(0, 100)}...' : content; } NotificationService().showChatNotification( title: notificationTitle, body: notificationBody, conversationKey: conversationKey, ); } catch (e, st) { developer.log('Error parsing chat message: $e', name: 'WebSocketService'); developer.log('Stack: $st', name: 'WebSocketService'); } } Future _handleChatMessageAck(Map data) async { final clientMessageId = data['clientMessageId']?.toString().trim() ?? ''; if (clientMessageId.isEmpty) { developer.log( 'Received message ACK without clientMessageId', name: 'WebSocketService', ); return; } await _databaseService.removeQueuedMessage(clientMessageId); await ChatService().markOutgoingMessageSynced(clientMessageId); } void _handleOtherClientMessage(String topic, Map data) { final type = data['type']; if (topic.contains('/tasks/') || type == 'task') { DartMQ().publish>(MQTopics.taskEvents, data); } else { developer.log( 'Unhandled client message type: $type on topic: $topic', name: 'WebSocketService', ); } } // --------------------------------------------------------------------------- // Public API // --------------------------------------------------------------------------- bool get isConnected => _isConnected; bool get isConnecting => _isConnecting; bool get isAuthenticated => _isAuthenticated; String? get authToken => _authToken; /// Connect to the WebSocket server Future connect() async { // Prevent overlapping connection attempts if (_isConnected) { developer.log( 'Already connected to WebSocket server', name: 'WebSocketService', ); return; } if (_isConnecting) { developer.log( 'Connection attempt already in progress - skipping', name: 'WebSocketService', ); return; } final wsUrl = _buildWebSocketUrl(); developer.log( 'Connecting to WebSocket server: $wsUrl', name: 'WebSocketService', ); _isConnecting = true; // Publish "not connected" status to UI while connecting (async to avoid build-phase issues) Future.microtask(() { DartMQ().publish(MQTopics.connectionStatus, false); }); try { // Ensure stable appId await _ensureAppId(); final channel = WebSocketChannel.connect(Uri.parse(wsUrl)); // Wait for the connection to be established await channel.ready; _wsChannel = channel; // Listen for incoming messages _wsSubscription = channel.stream.listen( _onWebSocketMessage, onError: (error) { developer.log('WebSocket error: $error', name: 'WebSocketService'); _handleWebSocketDisconnect(); }, onDone: () { developer.log( 'WebSocket connection closed', name: 'WebSocketService', ); _handleWebSocketDisconnect(); }, cancelOnError: false, ); // Stop any reconnection attempts since we're now connected _stopReconnectTimer(); developer.log( 'Connected to WebSocket server at $wsUrl', name: 'WebSocketService', ); // Run common post-connect setup (sets _isConnected, starts timers, auto-login) _onWebSocketConnected(); } catch (e) { _isConnecting = false; developer.log( 'Error connecting to WebSocket server: $e', name: 'WebSocketService', ); // Start reconnection attempts on connection failure _startReconnectTimer(); } } /// Send a message to the server. Buffers if not connected/authenticated /// or if sending fails due to an exception. void sendMessage(String topic, String message) { if (_isConnected && _isAuthenticated && _wsChannel != null) { final success = _sendWebSocket(topic, message); if (!success) { // Message could not be sent due to exception - buffer for retry developer.log( '>> BUFFERED (send failed): topic=$topic', name: 'WebSocketService', ); _messageBuffer.add(_BufferedMessage(topic, message)); } } else { developer.log( '>> BUFFERED (not ready): topic=$topic', name: 'WebSocketService', ); _messageBuffer.add(_BufferedMessage(topic, message)); } } /// Flush all buffered messages after successful authentication. /// Messages that fail to send are re-buffered for the next flush cycle. /// Clears all local jobs and related data, then notifies the server. Future _flushMessageBuffer() async { final initialBufferSize = _messageBuffer.length; final sentQueuedChatCount = await _flushQueuedChatMessages(); if (initialBufferSize > 0) { developer.log( 'Flushing ${_messageBuffer.length} buffered messages', name: 'WebSocketService', ); final messages = List<_BufferedMessage>.from(_messageBuffer); _messageBuffer.clear(); final failedMessages = <_BufferedMessage>[]; for (final msg in messages) { final success = _sendWebSocket(msg.topic, msg.jsonPayload); if (!success) { // Re-buffer failed messages for retry on next connection failedMessages.add(msg); } } // Add failed messages back to buffer for retry if (failedMessages.isNotEmpty) { developer.log( '${failedMessages.length} messages failed to send, re-buffering for retry', name: 'WebSocketService', ); _messageBuffer.addAll(failedMessages); } } // Clear all local jobs and related data before notifying server. // The server will send current jobs automatically. developer.log( 'Clearing local jobs and related data before buffer_flushed notification', name: 'WebSocketService', ); await _databaseService.clearAllJobsAndRelatedData(); // Notify server that buffer flush is complete final sentCount = (initialBufferSize - _messageBuffer.length) + sentQueuedChatCount; final bufferFlushedPayload = jsonEncode({ 'timestamp': DateTime.now().toIso8601String(), 'messageCount': sentCount, }); _sendWebSocket('/server/buffer_flushed', bufferFlushedPayload); } Future _flushQueuedChatMessages() async { final queuedMessages = await _databaseService.getQueuedMessages(); if (queuedMessages.isEmpty) { return 0; } developer.log( 'Flushing ${queuedMessages.length} queued chat messages', name: 'WebSocketService', ); var sentCount = 0; for (final message in queuedMessages) { final success = await _trySendQueuedChatMessage( message, incrementRetryOnFailure: true, ); if (success) { sentCount++; } } return sentCount; } Future _trySendQueuedChatMessage( QueuedMessage message, { bool incrementRetryOnFailure = false, }) async { if (!_isConnected || !_isAuthenticated || _wsChannel == null) { return false; } final success = _sendWebSocket(message.topic, jsonEncode(message.payload)); if (!success && incrementRetryOnFailure) { await _databaseService.updateMessageRetryCount( message.id, message.retryCount + 1, ); } return success; } /// Publish a chat message according to the backend contract. /// The message is stored locally and remains queued until the server confirms it. Future sendChatMessage({ required String sender, required String receiver, required String content, ChatContentType contentType = ChatContentType.text, String? jobId, String? jobNumber, }) async { final trimmedSender = sender.trim(); final trimmedReceiver = receiver.trim(); final trimmedContent = content.trim(); final normalizedJobId = jobId?.trim(); final normalizedJobNumber = jobNumber?.trim(); final hasJobContext = (normalizedJobId?.isNotEmpty ?? false) || (normalizedJobNumber?.isNotEmpty ?? false); if (trimmedSender.isEmpty || trimmedReceiver.isEmpty || trimmedContent.isEmpty) { developer.log( 'Cannot send chat message - missing required fields', name: 'WebSocketService', ); return null; } if (normalizedJobId != null && normalizedJobId.isNotEmpty && !_jobIdRegExp.hasMatch(normalizedJobId)) { developer.log( 'Cannot send chat message - jobId has invalid format: $normalizedJobId', name: 'WebSocketService', ); return null; } final payload = { 'sender': trimmedSender, 'receiver': trimmedReceiver, 'content': trimmedContent, }; final now = DateTime.now(); final clientMessageId = 'local-${now.microsecondsSinceEpoch}'; payload['messageId'] = clientMessageId; if (normalizedJobId != null && normalizedJobId.isNotEmpty) { payload['jobId'] = normalizedJobId; } if (normalizedJobNumber != null && normalizedJobNumber.isNotEmpty) { payload['jobNumber'] = normalizedJobNumber; } payload['contentType'] = chatContentTypeToString(contentType); const topic = '/server/message'; try { final message = ChatMessage( id: clientMessageId, content: trimmedContent, createdAt: now, direction: ChatDirection.outgoing, messageType: hasJobContext ? ChatMessageType.jobRelated : ChatMessageType.general, contentType: contentType, jobId: normalizedJobId?.isEmpty ?? true ? null : normalizedJobId, jobNumber: normalizedJobNumber?.isEmpty ?? true ? null : normalizedJobNumber, read: false, pendingSync: true, ); final queuedMessage = QueuedMessage( id: clientMessageId, topic: topic, payload: payload, createdAt: now, ); await _databaseService.queueMessage(queuedMessage); await ChatService().saveOutgoingMessage(message); final sentImmediately = await _trySendQueuedChatMessage(queuedMessage); if (!sentImmediately) { developer.log( 'Chat message $clientMessageId queued for retry after reconnect', name: 'WebSocketService', ); } return message; } catch (e, st) { developer.log('Error sending chat message: $e', name: 'WebSocketService'); developer.log('Stack: $st', name: 'WebSocketService'); return null; } } /// Subscribe to a topic (no-op for WebSocket - all messages arrive on one connection) void subscribe(String topic, [Function? callback]) { // WebSocket does not use topic subscriptions. // All messages are routed by the server to this connection after authentication. } /// Send login request Future login(String email, String password) async { final loginStartTime = DateTime.now(); final sessionId = loginStartTime.millisecondsSinceEpoch.toString(); developer.log('=== LOGIN METHOD CALLED ===', name: 'WebSocketService'); developer.log('Email: $email', name: 'WebSocketService'); developer.log('isConnected: $_isConnected', name: 'WebSocketService'); developer.log( 'wsChannel: ${_wsChannel != null ? "exists" : "null"}', name: 'WebSocketService', ); await _ensureAppId(); developer.log('AppId: $_appId', name: 'WebSocketService'); if (!_isConnected || _wsChannel == null) { developer.log( 'LOGIN ABORTED: Not connected to server', name: 'WebSocketService', ); _lastAuthResponse = { 'success': false, 'message': 'Nicht mit Server verbunden', 'sessionId': sessionId, 'timestamp': loginStartTime.toIso8601String(), }; DartMQ().publish>( MQTopics.authResponse, _lastAuthResponse!, ); return; } final loginData = {'email': email, 'password': password}; try { const topic = '/server/login'; final jsonPayload = jsonEncode(loginData); developer.log( 'Sending login message to $topic', name: 'WebSocketService', ); // Send login directly (not via sendMessage which buffers until authenticated) _sendWebSocket(topic, jsonPayload); developer.log( 'Login message sent successfully', name: 'WebSocketService', ); } catch (e, st) { developer.log('Error sending login: $e', name: 'WebSocketService'); developer.log('Stack: $st', name: 'WebSocketService'); _lastAuthResponse = { 'success': false, 'message': 'Fehler beim Senden der Anmeldedaten', 'error': e.toString(), 'sessionId': sessionId, 'timestamp': DateTime.now().toIso8601String(), }; DartMQ().publish>( MQTopics.authResponse, _lastAuthResponse!, ); } } /// Retry login with saved credentials (called from UI after auth timeout) Future retryLoginWithSavedCredentials() async { if (!_isConnected || _wsChannel == null) { developer.log( 'Cannot retry login: not connected to server', name: 'WebSocketService', ); return; } final credentials = await _databaseService.loadCredentials(); if (credentials != null) { developer.log( 'Retrying login with saved credentials for ${credentials.email}', name: 'WebSocketService', ); await login(credentials.email, credentials.password); } else { developer.log( 'Cannot retry login: no saved credentials found', name: 'WebSocketService', ); } } /// Logout user Future logout() async { _isAuthenticated = false; _authToken = null; // Delete saved credentials to prevent auto-login await _databaseService.deleteCredentials(); // Stop location tracking LocationService().stopTracking(); } /// Disconnect from WebSocket server Future disconnect() async { // Stop location tracking LocationService().stopTracking(); if (_wsChannel != null) { final completer = Completer(); _disconnectCompleter = completer; try { await _wsChannel!.sink.close(ws_status.normalClosure); } catch (e, st) { developer.log('Error during disconnect: $e', name: 'WebSocketService'); developer.log('Stack: $st', name: 'WebSocketService'); if (!completer.isCompleted) { completer.complete(); } } try { await completer.future.timeout(const Duration(seconds: 3)); } catch (_) {} _wsSubscription?.cancel(); _wsSubscription = null; _wsChannel = null; _disconnectCompleter = null; } _isConnected = false; _isAuthenticated = false; _authToken = null; _stopReconnectTimer(); _lastAuthResponse = null; _messageBuffer.clear(); Future.microtask(() { DartMQ().publish(MQTopics.connectionStatus, false); }); } /// Reset service state without disposing (useful for logout) Future reset() async { await disconnect(); } /// Send task completion event to server. /// Messages are buffered if offline and sent automatically when reconnected. Future sendTaskCompleted({ required String taskId, String? taskType, Map? extraData, }) async { final String normalizedType = (taskType ?? 'UNKNOWN').toUpperCase(); const String destination = '/server/task_completed'; final payload = { 'taskId': taskId, 'taskType': normalizedType, }; if (extraData != null && extraData.isNotEmpty) { payload['extraData'] = extraData; } try { final jsonPayload = jsonEncode(payload); // sendMessage buffers automatically if not connected/authenticated sendMessage(destination, jsonPayload); } catch (e, st) { developer.log( 'Error sending task completion: $e', name: 'WebSocketService', ); developer.log('Stack: $st', name: 'WebSocketService'); } } /// Dispose resources void dispose() { _stopReconnectTimer(); disconnect(); } } class _BufferedMessage { final String topic; final String jsonPayload; _BufferedMessage(this.topic, this.jsonPayload); } // Backward compatibility typedef typedef StompService = WebSocketService;