package chats.data.repository import chats.data.remote.api.ChatApi import chats.data.remote.api.SendMessageRequest import chats.data.remote.dto.ChatDto import chats.data.remote.dto.MessageDto import chats.data.remote.dto.MediaItemDto import chats.data.remote.dto.ReactionDto import chats.domain.model.Chat import chats.domain.model.Message import chats.domain.model.MediaType import chats.domain.repository.ChatRepository import core.network.ServerConfig import core.security.TokenManager import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MultipartBody import okhttp3.RequestBody.Companion.asRequestBody import javax.inject.Inject import kotlinx.coroutines.flow.map class ChatRepositoryImpl @Inject constructor( private val api: ChatApi, private val tokenManager: TokenManager, private val serverConfig: ServerConfig, private val messageDao: core.database.data.MessageDao, private val hubClient: chats.data.remote.signalr.ChatHubClient ) : ChatRepository { private val gson = com.google.gson.Gson() override suspend fun getChats(): List { val currentUserId = tokenManager.getUserId() ?: "" val baseUrl = serverConfig.getBaseUrl().removeSuffix("/api/") return api.getChats().map { it.toDomain(currentUserId, baseUrl) } } override fun getMessagesFlow(chatId: String): kotlinx.coroutines.flow.Flow> { val baseUrl = serverConfig.getBaseUrl().removeSuffix("/api/") return messageDao.getMessages(chatId).map { entities: List -> entities.map { it.toDomain(baseUrl, gson) } } } override suspend fun getMessages(chatId: String): List { val baseUrl = serverConfig.getBaseUrl().removeSuffix("/api/") return try { val messages = api.getMessages(chatId) // Save to DB messageDao.insertMessages(messages.map { msg: MessageDto -> msg.toEntity(baseUrl, gson) }) messages.map { it.toDomain(baseUrl) } } catch (e: Exception) { // If network fails, caller should ideally use the Flow from DB emptyList() } } override suspend fun sendMessage( chatId: String, content: String?, type: String, attachments: List? ): Message { val request = SendMessageRequest( content = content, type = type, attachments = attachments ) val baseUrl = serverConfig.getBaseUrl().removeSuffix("/api/") return api.sendMessage(chatId, request).toDomain(baseUrl) } override suspend fun addReaction(messageId: String, emoji: String) { api.addReaction(messageId, emoji) } override suspend fun sendTypingStatus(chatId: String) { api.sendTypingStatus(chatId) } override suspend fun markMessagesAsRead(chatId: String, lastMessageId: String, lastReadSequenceId: Int) { val request = chats.data.remote.signalr.ReadMessagesRequest( chatId = chatId, lastReadMessageId = lastMessageId, lastReadSequenceId = lastReadSequenceId ) hubClient.readMessages(request) } override suspend fun saveMessage(message: Message) { messageDao.insertMessages(listOf(message.toEntity(gson))) } override suspend fun deleteLocalMessage(messageId: String) { messageDao.deleteMessage(messageId) } override suspend fun uploadMedia(file: java.io.File): String { val requestFile = file.asRequestBody("image/*".toMediaTypeOrNull()) val body = MultipartBody.Part.createFormData("file", file.name, requestFile) return api.uploadFile(body).url } override suspend fun getTrendingGifs(page: Int): List { return api.getTrendingGifs(page).data.data } override suspend fun searchGifs(query: String, page: Int): List { return api.searchGifs(query, page).data.data } override suspend fun getGifCategories(): List { return api.getGifCategories().data.categories } } // Mappers fun ChatDto.toDomain(currentUserId: String, baseUrl: String): Chat { val chatName = name ?: if (type == "personal") { members.firstOrNull { it.userId != currentUserId }?.user?.displayName ?: "Unknown Chat" } else "Group Chat" val chatAvatar = (avatar ?: if (type == "personal") { members.firstOrNull { it.userId != currentUserId }?.user?.avatarUrl } else null)?.ensureAbsoluteUrl(baseUrl) return Chat( id = id, type = type, name = chatName, avatar = chatAvatar, unreadCount = unreadCount, lastMessage = messages.firstOrNull()?.toDomain(baseUrl) ) } fun Message.toEntity(gson: com.google.gson.Gson): core.database.data.MessageEntity { return core.database.data.MessageEntity( id = id, chatId = chatId, senderId = senderId, senderName = senderName, senderAvatar = senderAvatar, content = content, sequenceId = sequenceId, createdAt = createdAt, mediaType = mediaType.name.lowercase(), mediaJson = gson.toJson(media), reactionsJson = gson.toJson(reactions), isRead = isRead, replyToId = replyTo?.id ) } fun MessageDto.toDomain(baseUrl: String): Message { val domainMediaType = when (type) { "gif" -> MediaType.GIF "image", "photo" -> MediaType.IMAGE "video" -> MediaType.VIDEO "audio", "voice" -> MediaType.AUDIO "file" -> MediaType.FILE else -> when (media.firstOrNull()?.type) { "image", "photo" -> MediaType.IMAGE "video" -> MediaType.VIDEO "audio", "voice" -> MediaType.AUDIO "file" -> MediaType.FILE else -> MediaType.TEXT } } return Message( id = id, chatId = chatId ?: "", senderId = senderId ?: "", senderName = sender?.displayName ?: "Unknown", senderAvatar = sender?.avatarUrl?.ensureAbsoluteUrl(baseUrl), content = content, sequenceId = sequenceId ?: 0, createdAt = createdAt ?: "", media = media.map { chats.domain.model.Media( id = it.id, type = it.type, url = it.url.ensureAbsoluteUrl(baseUrl), filename = it.filename, size = it.size, duration = it.duration ) }, mediaType = domainMediaType, reactions = reactions?.associate { it.emoji to it.count } ?: emptyMap(), isRead = true, // Network messages are usually considered read when fetched or handled by server replyTo = replyTo?.toDomain(baseUrl) ) } fun MessageDto.toEntity(baseUrl: String, gson: com.google.gson.Gson): core.database.data.MessageEntity { return core.database.data.MessageEntity( id = id, chatId = chatId ?: "", senderId = senderId ?: "", senderName = sender?.displayName ?: "Unknown", senderAvatar = sender?.avatarUrl?.ensureAbsoluteUrl(baseUrl), content = content, sequenceId = sequenceId ?: 0, createdAt = createdAt ?: "", mediaType = type ?: "text", mediaJson = gson.toJson(media), reactionsJson = gson.toJson(reactions), isRead = true, replyToId = replyTo?.id ) } fun core.database.data.MessageEntity.toDomain(baseUrl: String, gson: com.google.gson.Gson): Message { val mediaTypeEnum = when (mediaType) { "gif" -> MediaType.GIF "image", "photo" -> MediaType.IMAGE "video" -> MediaType.VIDEO "audio", "voice" -> MediaType.AUDIO "file" -> MediaType.FILE else -> MediaType.TEXT } val mediaTypeToken = object : com.google.gson.reflect.TypeToken>() {}.type val mediaDtos: List = gson.fromJson(mediaJson, mediaTypeToken) ?: emptyList() val reactionsTypeToken = object : com.google.gson.reflect.TypeToken>() {}.type val reactionDtos: List = gson.fromJson(reactionsJson, reactionsTypeToken) ?: emptyList() return Message( id = id, chatId = chatId, senderId = senderId, senderName = senderName, senderAvatar = senderAvatar, content = content, sequenceId = sequenceId, createdAt = createdAt, media = mediaDtos.map { chats.domain.model.Media( id = it.id, type = it.type, url = it.url.ensureAbsoluteUrl(baseUrl), filename = it.filename, size = it.size, duration = it.duration ) }, mediaType = mediaTypeEnum, reactions = reactionDtos.associate { it.emoji to it.count }, isRead = isRead ) } fun String.ensureAbsoluteUrl(baseUrl: String): String { return if (this.startsWith("http")) { this } else { val base = baseUrl.removeSuffix("/") val path = if (this.startsWith("/")) this else "/$this" "$base$path" } }