package chats.data.workers import android.content.Context import android.util.Log import androidx.hilt.work.HiltWorker import androidx.work.* import chats.data.local.dao.ChatDao import chats.data.local.dao.MessageDao import chats.data.remote.dto.* import chats.data.repository.toDomain import chats.domain.model.MessageStatus import core.security.TokenManager import dagger.assisted.Assisted import dagger.assisted.AssistedInject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.io.IOException import java.net.SocketTimeoutException import java.util.concurrent.TimeUnit /** * Worker для периодической синхронизации чатов и сообщений. * Запускается при наличии сети для получения новых сообщений с сервера. */ @HiltWorker class ChatSyncWorker @AssistedInject constructor( @Assisted context: Context, @Assisted params: WorkerParameters, private val chatDao: ChatDao, private val messageDao: MessageDao, private val chatApi: chats.data.remote.api.ChatApi, private val tokenManager: TokenManager, private val serverConfig: core.network.ServerConfig ) : CoroutineWorker(context, params) { companion object { const val WORK_TAG = "chat_sync_worker" const val KEY_CHAT_ID = "chat_id" const val KEY_CURSOR = "cursor" /** * Создает периодический запрос на синхронизацию всех чатов */ fun createPeriodicWorkRequest(): PeriodicWorkRequest { val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build() return PeriodicWorkRequestBuilder( repeatInterval = 15, repeatIntervalTimeUnit = TimeUnit.MINUTES ) .setConstraints(constraints) .addTag(WORK_TAG) .build() } /** * Создает разовый запрос на синхронизацию конкретного чата */ fun createOneTimeWorkRequest(chatId: String? = null, cursor: String? = null): OneTimeWorkRequest { val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build() val inputData = workDataOf( KEY_CHAT_ID to chatId, KEY_CURSOR to cursor ) return OneTimeWorkRequestBuilder() .setConstraints(constraints) .setInputData(inputData) .addTag(WORK_TAG) .build() } } override suspend fun doWork(): Result = withContext(Dispatchers.IO) { Log.d("ChatSyncWorker", "Starting chat sync") try { // 1. Синхронизируем список чатов syncChats() // 2. Если указан chatId - синхронизируем сообщения val chatId = inputData.getString(KEY_CHAT_ID) if (chatId != null) { syncMessagesForChat(chatId) } Log.i("ChatSyncWorker", "Sync completed successfully") Result.success() } catch (e: IOException) { Log.w("ChatSyncWorker", "Network error during sync: ${e.message}") Result.retry() } catch (e: SocketTimeoutException) { Log.w("ChatSyncWorker", "Timeout during sync: ${e.message}") Result.retry() } catch (e: Exception) { Log.e("ChatSyncWorker", "Error during sync", e) Result.failure() } } private suspend fun syncChats() { val remoteChats = chatApi.getChats() val localChats = mutableListOf() val currentUserId = tokenManager.getUserId() ?: "" val baseUrl = serverConfig.getBaseUrl().removeSuffix("/api/") val gson = com.google.gson.Gson() remoteChats.forEach { dto -> val existingChat = chatDao.getChatByRemoteId(dto.id) val lastMessage = dto.messages.firstOrNull() val timestamp = lastMessage?.createdAt?.let { try { java.time.ZonedDateTime.parse(it).toInstant().toEpochMilli() } catch (e: Exception) { null } } val lastMessageDomain = lastMessage?.toDomain(currentUserId, baseUrl) val chatEntity = chats.data.local.database.ChatEntity( localId = existingChat?.localId ?: java.util.UUID.randomUUID().toString(), remoteId = dto.id, type = dto.type, name = dto.name ?: "", avatar = dto.avatar, unreadCount = dto.unreadCount, lastMessageText = lastMessage?.content, lastMessageTimestamp = timestamp, lastMessageJson = lastMessageDomain?.let { gson.toJson(it) } ) localChats.add(chatEntity) } // Если сервер не вернул Избранное — создаём/получаем его отдельно val hasFavorites = remoteChats.any { it.type == "favorites" } if (!hasFavorites) { try { val favDto = chatApi.getOrCreateFavorites() val existingChat = chatDao.getChatByRemoteId(favDto.id) val favTimestamp = favDto.messages.firstOrNull()?.createdAt?.let { try { java.time.ZonedDateTime.parse(it).toInstant().toEpochMilli() } catch (e: Exception) { null } } val favLastMessageDomain = favDto.messages.firstOrNull()?.toDomain(currentUserId, baseUrl) val favEntity = chats.data.local.database.ChatEntity( localId = existingChat?.localId ?: java.util.UUID.randomUUID().toString(), remoteId = favDto.id, type = favDto.type, name = favDto.name ?: "Избранное", avatar = favDto.avatar, unreadCount = favDto.unreadCount, lastMessageText = favDto.messages.firstOrNull()?.content, lastMessageTimestamp = favTimestamp, lastMessageJson = favLastMessageDomain?.let { gson.toJson(it) } ) localChats.add(favEntity) Log.d("ChatSyncWorker", "Created favorites chat: ${favDto.id}") } catch (e: Exception) { Log.e("ChatSyncWorker", "Failed to get or create favorites", e) } } chatDao.insertChats(localChats) Log.d("ChatSyncWorker", "Synced ${localChats.size} chats") } private suspend fun syncMessagesForChat(chatId: String) { val lastMessage = messageDao.getMessagesByStatus( chatId = chatId, statuses = listOf(MessageStatus.SENT, MessageStatus.DELIVERED, MessageStatus.READ) ).maxByOrNull { it.sequenceId } val cursor = lastMessage?.serverId val messages = chatApi.getMessages(chatId, cursor = cursor, limit = 50) if (messages.isNotEmpty()) { saveMessagesToLocal(messages, chatId) Log.d("ChatSyncWorker", "Synced ${messages.size} messages for chat $chatId") } } private suspend fun saveMessagesToLocal( messages: List, chatId: String ) { val currentUserId = tokenManager.getUserId() ?: "" val gson = com.google.gson.Gson() val baseUrl = serverConfig.getBaseUrl().removeSuffix("/api/") messages.forEach { dto -> val existing = messageDao.getMessageByServerId(dto.id) if (existing == null) { val entity = chats.data.local.database.MessageEntity( localId = java.util.UUID.randomUUID().toString(), serverId = dto.id, idempotencyKey = dto.id, chatId = chatId, senderId = dto.senderId ?: dto.sender?.id ?: currentUserId, senderName = dto.sender?.displayName ?: dto.sender?.username ?: "", senderAvatar = dto.sender?.avatarUrl?.let { if (it.startsWith("http")) it else "$baseUrl$it" }, content = dto.content, sequenceId = dto.sequenceId?.toLong() ?: 0L, createdAt = dto.createdAt ?: java.time.ZonedDateTime.now().toString(), mediaType = dto.type ?: "TEXT", mediaJson = gson.toJson(dto.media.map { mediaItem -> chats.domain.model.Media( id = mediaItem.id, type = mediaItem.type, url = (mediaItem.url as? String ?: "").let { url -> if (url.startsWith("http")) url else "$baseUrl$url" }, filename = mediaItem.filename, size = mediaItem.size, duration = mediaItem.duration ) }), reactionsJson = gson.toJson( dto.reactions?.associate { it.emoji to it.count } ?: emptyMap() ), status = if (dto.senderId == currentUserId) MessageStatus.SENT else MessageStatus.DELIVERED, createdAtMillis = System.currentTimeMillis(), updatedAtMillis = System.currentTimeMillis() ) messageDao.insertMessage(entity) } } } }