package com.seven.ikar.kotlin.core.network

import com.seven.ikar.kotlin.core.model.AndroidAppReleaseDto
import com.seven.ikar.kotlin.core.model.AddChatMembersRequest
import com.seven.ikar.kotlin.core.model.ArgusManifestDto
import com.seven.ikar.kotlin.core.model.AttachmentDto
import com.seven.ikar.kotlin.core.model.AuthSessionDto
import com.seven.ikar.kotlin.core.model.BotSummaryDto
import com.seven.ikar.kotlin.core.model.BotCommandDto
import com.seven.ikar.kotlin.core.model.ChatDetailsDto
import com.seven.ikar.kotlin.core.model.ChatFolderDto
import com.seven.ikar.kotlin.core.model.ChatInfoDto
import com.seven.ikar.kotlin.core.model.ChatMessagesPageDto
import com.seven.ikar.kotlin.core.model.ChatSearchPageDto
import com.seven.ikar.kotlin.core.model.ChatSearchResultDto
import com.seven.ikar.kotlin.core.model.ChatStateRequest
import com.seven.ikar.kotlin.core.model.ChatSummaryDto
import com.seven.ikar.kotlin.core.model.ChatInviteLinkDto
import com.seven.ikar.kotlin.core.model.ChatJoinRequestDto
import com.seven.ikar.kotlin.core.model.ChatTopicDto
import com.seven.ikar.kotlin.core.model.CreateChatInviteLinkRequest
import com.seven.ikar.kotlin.core.model.ContactMatchDto
import com.seven.ikar.kotlin.core.model.CreateBotRequest
import com.seven.ikar.kotlin.core.model.CreateChannelRequest
import com.seven.ikar.kotlin.core.model.CreateChatTopicRequest
import com.seven.ikar.kotlin.core.model.CreateStoryRequest
import com.seven.ikar.kotlin.core.model.CreatedBotDto
import com.seven.ikar.kotlin.core.model.CreateDirectChatRequest
import com.seven.ikar.kotlin.core.model.CreateGroupChatRequest
import com.seven.ikar.kotlin.core.model.DownloadedAttachmentFile
import com.seven.ikar.kotlin.core.model.ForwardMessageRequest
import com.seven.ikar.kotlin.core.model.MessageDto
import com.seven.ikar.kotlin.core.model.MessageSearchPageDto
import com.seven.ikar.kotlin.core.model.ModerationLogDto
import com.seven.ikar.kotlin.core.model.PhoneCodeChallengeDto
import com.seven.ikar.kotlin.core.model.PrivacySettingsDto
import com.seven.ikar.kotlin.core.model.RegenerateBotTokenResponseDto
import com.seven.ikar.kotlin.core.model.RequestPhoneCodeRequest
import com.seven.ikar.kotlin.core.model.RegisterPushDeviceRequest
import com.seven.ikar.kotlin.core.model.RefreshSessionRequest
import com.seven.ikar.kotlin.core.model.ResolveChatJoinRequestRequest
import com.seven.ikar.kotlin.core.model.ResolveContactsRequest
import com.seven.ikar.kotlin.core.model.SendMessageRequest
import com.seven.ikar.kotlin.core.model.SetMessageReactionRequest
import com.seven.ikar.kotlin.core.model.SetBotCommandsRequest
import com.seven.ikar.kotlin.core.model.StoryDto
import com.seven.ikar.kotlin.core.model.StoryViewDto
import com.seven.ikar.kotlin.core.model.StoryVisibility
import com.seven.ikar.kotlin.core.model.UpdateBotRequest
import com.seven.ikar.kotlin.core.model.UpdateChatMemberPermissionsRequest
import com.seven.ikar.kotlin.core.model.UpdateMessageRequest
import com.seven.ikar.kotlin.core.model.UpdateMessageReceiptRequest
import com.seven.ikar.kotlin.core.model.UpdateMyProfileRequest
import com.seven.ikar.kotlin.core.model.UpdatePrivacySettingsRequest
import com.seven.ikar.kotlin.core.model.UserSessionDto
import com.seven.ikar.kotlin.core.model.UserSummaryDto
import com.seven.ikar.kotlin.core.model.VerifyPhoneCodeRequest
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody
import okhttp3.OkHttpClient
import okhttp3.RequestBody
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okio.BufferedSink
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.net.URLEncoder
import java.nio.charset.StandardCharsets

class ApiException(val statusCode: Int, override val message: String) : RuntimeException(message)

class UploadFileSource(
    val fileName: String,
    val contentType: String?,
    val contentLength: Long = -1L,
    private val onBytesWritten: ((Long) -> Unit)? = null,
    private val openStream: () -> InputStream
) {
    fun asRequestBody(): RequestBody {
        val mediaType = contentType?.toMediaTypeOrNull() ?: "application/octet-stream".toMediaType()
        return object : RequestBody() {
            override fun contentType() = mediaType
            override fun contentLength(): Long = this@UploadFileSource.contentLength.takeIf { it >= 0L } ?: -1L

            override fun writeTo(sink: BufferedSink) {
                openStream().use { input ->
                    val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
                    var bytesWritten = 0L
                    while (true) {
                        val read = input.read(buffer)
                        if (read < 0) {
                            return
                        }
                        sink.write(buffer, 0, read)
                        bytesWritten += read
                        onBytesWritten?.invoke(bytesWritten)
                    }
                }
            }
        }
    }
}

class ApiClient(
    private val httpClientProvider: () -> OkHttpClient,
    private val json: Json,
    private val clientVersionProvider: ClientVersionProvider
) {
    constructor(
        httpClient: OkHttpClient,
        json: Json,
        clientVersionProvider: ClientVersionProvider
    ) : this({ httpClient }, json, clientVersionProvider)

    private val httpClient: OkHttpClient
        get() = httpClientProvider()

    suspend fun requestPhoneCode(phoneNumber: String): PhoneCodeChallengeDto =
        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/auth/request-code"))
                .post(
                    json.encodeToString(
                        RequestPhoneCodeRequest.serializer(),
                        RequestPhoneCodeRequest(phoneNumber = phoneNumber)
                    ).toRequestBody(JSON)
                )
                .withClientVersionHeaders()
                .build(),
            PhoneCodeChallengeDto.serializer()
        )

    suspend fun verifyPhoneCode(
        challengeId: String,
        phoneNumber: String,
        code: String,
        displayName: String?
    ): AuthSessionDto =
        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/auth/verify-code"))
                .post(
                    json.encodeToString(
                        VerifyPhoneCodeRequest.serializer(),
                        VerifyPhoneCodeRequest(
                            challengeId = challengeId,
                            phoneNumber = phoneNumber,
                            code = code,
                            displayName = displayName?.takeIf { it.isNotBlank() }
                        )
                    ).toRequestBody(JSON)
                )
                .withClientVersionHeaders()
                .build(),
            AuthSessionDto.serializer()
        )

    suspend fun refreshSession(refreshToken: String): AuthSessionDto =
        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/auth/refresh"))
                .post(
                    json.encodeToString(
                        RefreshSessionRequest.serializer(),
                        RefreshSessionRequest(refreshToken)
                    ).toRequestBody(JSON)
                )
                .withClientVersionHeaders()
                .build(),
            AuthSessionDto.serializer()
        )

    suspend fun getChats(accessToken: String): List<ChatSummaryDto> =
        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/chats"))
                .get()
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            kotlinx.serialization.builtins.ListSerializer(ChatSummaryDto.serializer())
        )

    suspend fun getChatFolders(accessToken: String): List<ChatFolderDto> =
        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/chats/folders"))
                .get()
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            kotlinx.serialization.builtins.ListSerializer(ChatFolderDto.serializer())
        )

    suspend fun searchChats(accessToken: String, query: String, filter: String? = null, limit: Int = 50): List<ChatSearchResultDto> =
        searchChatsPage(
            accessToken = accessToken,
            query = query,
            filter = filter,
            limit = limit
        ).results

    suspend fun searchChatsPage(
        accessToken: String,
        query: String,
        filter: String? = null,
        type: String? = null,
        page: Int = 1,
        limit: Int = 50,
        dateFrom: String? = null,
        dateTo: String? = null,
        author: String? = null
    ): ChatSearchPageDto =
        executeJsonRequest(
            Request.Builder()
                .url(
                    ServerConfig.absoluteMessengerUrl(
                        buildSearchPath(
                            basePath = "/api/chats/search/page",
                            query = query,
                            filter = filter,
                            type = type,
                            page = page,
                            limit = limit,
                            dateFrom = dateFrom,
                            dateTo = dateTo,
                            author = author
                        )
                    )
                )
                .get()
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            ChatSearchPageDto.serializer()
        )

    suspend fun getChat(chatId: String, accessToken: String): ChatDetailsDto =
        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/chats/$chatId"))
                .get()
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            ChatDetailsDto.serializer()
        )

    suspend fun getChatInfo(accessToken: String, chatId: String): ChatInfoDto =
        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/chats/$chatId/info"))
                .get()
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            ChatInfoDto.serializer()
        )

    suspend fun getChatInviteLinks(accessToken: String, chatId: String): List<ChatInviteLinkDto> =
        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/chats/$chatId/invite-links"))
                .get()
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            kotlinx.serialization.builtins.ListSerializer(ChatInviteLinkDto.serializer())
        )

    suspend fun createChatInviteLink(
        accessToken: String,
        chatId: String,
        title: String? = null,
        createsJoinRequest: Boolean = false
    ): ChatInviteLinkDto =
        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/chats/$chatId/invite-links"))
                .post(
                    json.encodeToString(
                        CreateChatInviteLinkRequest.serializer(),
                        CreateChatInviteLinkRequest(title = title?.takeIf { it.isNotBlank() }, createsJoinRequest = createsJoinRequest)
                    ).toRequestBody(JSON)
                )
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            ChatInviteLinkDto.serializer()
        )

    suspend fun revokeChatInviteLink(accessToken: String, chatId: String, inviteLinkId: String) =
        executeUnitRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/chats/$chatId/invite-links/$inviteLinkId"))
                .delete()
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build()
        )

    suspend fun joinChatByInviteLink(accessToken: String, token: String): ChatDetailsDto =
        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/chats/join/${encodeQuery(token)}"))
                .post("".toRequestBody(null))
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            ChatDetailsDto.serializer()
        )

    suspend fun getChatJoinRequests(accessToken: String, chatId: String): List<ChatJoinRequestDto> =
        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/chats/$chatId/join-requests"))
                .get()
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            kotlinx.serialization.builtins.ListSerializer(ChatJoinRequestDto.serializer())
        )

    suspend fun resolveChatJoinRequest(
        accessToken: String,
        chatId: String,
        joinRequestId: String,
        approve: Boolean
    ): ChatJoinRequestDto =
        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/chats/$chatId/join-requests/$joinRequestId"))
                .post(
                    json.encodeToString(
                        ResolveChatJoinRequestRequest.serializer(),
                        ResolveChatJoinRequestRequest(approve = approve)
                    ).toRequestBody(JSON)
                )
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            ChatJoinRequestDto.serializer()
        )

    suspend fun getChatTopics(accessToken: String, chatId: String): List<ChatTopicDto> =
        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/chats/$chatId/topics"))
                .get()
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            kotlinx.serialization.builtins.ListSerializer(ChatTopicDto.serializer())
        )

    suspend fun createChatTopic(accessToken: String, chatId: String, title: String, icon: String? = null): ChatTopicDto =
        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/chats/$chatId/topics"))
                .post(
                    json.encodeToString(
                        CreateChatTopicRequest.serializer(),
                        CreateChatTopicRequest(title = title, icon = icon?.takeIf { it.isNotBlank() })
                    ).toRequestBody(JSON)
                )
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            ChatTopicDto.serializer()
        )

    suspend fun getChatMessagesPage(
        accessToken: String,
        chatId: String,
        beforeMessageId: String,
        limit: Int = 50
    ): ChatMessagesPageDto =
        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/chats/$chatId/messages?beforeMessageId=$beforeMessageId&limit=$limit"))
                .get()
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            ChatMessagesPageDto.serializer()
        )

    suspend fun searchChatMessages(
        accessToken: String,
        chatId: String,
        query: String,
        filter: String? = null,
        limit: Int = 50
    ): List<MessageDto> =
        searchChatMessagesPage(
            accessToken = accessToken,
            chatId = chatId,
            query = query,
            filter = filter,
            limit = limit
        ).messages

    suspend fun searchChatMessagesPage(
        accessToken: String,
        chatId: String,
        query: String,
        filter: String? = null,
        type: String? = null,
        page: Int = 1,
        limit: Int = 50,
        dateFrom: String? = null,
        dateTo: String? = null,
        author: String? = null
    ): MessageSearchPageDto =
        executeJsonRequest(
            Request.Builder()
                .url(
                    ServerConfig.absoluteMessengerUrl(
                        buildSearchPath(
                            basePath = "/api/chats/$chatId/messages/search/page",
                            query = query,
                            filter = filter,
                            type = type,
                            page = page,
                            limit = limit,
                            dateFrom = dateFrom,
                            dateTo = dateTo,
                            author = author
                        )
                    )
                )
                .get()
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            MessageSearchPageDto.serializer()
        )

    suspend fun updateChatState(accessToken: String, chatId: String, request: ChatStateRequest): ChatSummaryDto =
        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/chats/$chatId/state"))
                .put(
                    json.encodeToString(
                        ChatStateRequest.serializer(),
                        request
                    ).toRequestBody(JSON)
                )
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            ChatSummaryDto.serializer()
        )

    suspend fun updateChatMemberPermissions(
        accessToken: String,
        chatId: String,
        memberUserId: String,
        request: UpdateChatMemberPermissionsRequest
    ): ChatDetailsDto =
        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/chats/$chatId/members/$memberUserId/permissions"))
                .put(
                    json.encodeToString(
                        UpdateChatMemberPermissionsRequest.serializer(),
                        request
                    ).toRequestBody(JSON)
                )
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            ChatDetailsDto.serializer()
        )

    suspend fun addChatMembers(
        accessToken: String,
        chatId: String,
        request: AddChatMembersRequest
    ): ChatDetailsDto =
        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/chats/$chatId/members"))
                .post(
                    json.encodeToString(
                        AddChatMembersRequest.serializer(),
                        request
                    ).toRequestBody(JSON)
                )
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            ChatDetailsDto.serializer()
        )

    suspend fun getChatModerationLog(accessToken: String, chatId: String, limit: Int = 100): List<ModerationLogDto> =
        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/chats/$chatId/moderation-log?limit=$limit"))
                .get()
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            kotlinx.serialization.builtins.ListSerializer(ModerationLogDto.serializer())
        )

    suspend fun deleteChat(accessToken: String, chatId: String, forEveryone: Boolean) =
        executeUnitRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/chats/$chatId?forEveryone=$forEveryone"))
                .delete()
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build()
        )

    suspend fun sendMessage(
        chatId: String,
        text: String,
        accessToken: String,
        replyToMessageId: String? = null,
        topicId: String? = null,
        hasProtectedContent: Boolean? = null,
        ttlSeconds: Int? = null
    ): MessageDto =
        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/chats/$chatId/messages"))
                .post(
                    json.encodeToString(
                        SendMessageRequest.serializer(),
                        SendMessageRequest(
                            text = text,
                            replyToMessageId = replyToMessageId,
                            topicId = topicId,
                            hasProtectedContent = hasProtectedContent,
                            ttlSeconds = ttlSeconds
                        )
                    ).toRequestBody(JSON)
                )
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            MessageDto.serializer()
        )

    suspend fun getBotCommands(accessToken: String, userId: String): List<BotCommandDto> =
        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/users/$userId/bot-commands"))
                .get()
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            kotlinx.serialization.builtins.ListSerializer(BotCommandDto.serializer())
        )

    suspend fun clearChatHistory(accessToken: String, chatId: String) =
        executeUnitRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/chats/$chatId/history"))
                .delete()
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build()
        )

    suspend fun updateMessage(accessToken: String, chatId: String, messageId: String, text: String): MessageDto =
        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/chats/$chatId/messages/$messageId"))
                .put(
                    json.encodeToString(
                        UpdateMessageRequest.serializer(),
                        UpdateMessageRequest(text)
                    ).toRequestBody(JSON)
                )
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            MessageDto.serializer()
        )

    suspend fun deleteMessage(accessToken: String, chatId: String, messageId: String): MessageDto =
        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/chats/$chatId/messages/$messageId"))
                .delete()
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            MessageDto.serializer()
        )

    suspend fun pinMessage(accessToken: String, chatId: String, messageId: String): MessageDto =
        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/chats/$chatId/messages/$messageId/pin"))
                .put("".toRequestBody(null))
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            MessageDto.serializer()
        )

    suspend fun unpinMessage(accessToken: String, chatId: String, messageId: String): MessageDto =
        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/chats/$chatId/messages/$messageId/pin"))
                .delete()
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            MessageDto.serializer()
        )

    suspend fun uploadAttachment(
        accessToken: String,
        chatId: String,
        text: String?,
        replyToMessageId: String?,
        file: UploadFileSource
    ): MessageDto = withContext(Dispatchers.IO) {
        val multipart = MultipartBody.Builder()
            .setType(MultipartBody.FORM)
            .apply {
                text?.takeIf { it.isNotBlank() }?.let { addFormDataPart("text", it) }
                replyToMessageId?.takeIf { it.isNotBlank() }?.let { addFormDataPart("replyToMessageId", it) }
                addFormDataPart("file", file.fileName, file.asRequestBody())
            }
            .build()

        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/chats/$chatId/attachments"))
                .post(multipart)
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            MessageDto.serializer()
        )
    }

    suspend fun uploadAttachmentAlbum(
        accessToken: String,
        chatId: String,
        text: String?,
        replyToMessageId: String?,
        files: List<UploadFileSource>
    ): MessageDto = withContext(Dispatchers.IO) {
        check(files.isNotEmpty()) { "At least one file is required." }

        val multipart = MultipartBody.Builder()
            .setType(MultipartBody.FORM)
            .apply {
                text?.takeIf { it.isNotBlank() }?.let { addFormDataPart("text", it) }
                replyToMessageId?.takeIf { it.isNotBlank() }?.let { addFormDataPart("replyToMessageId", it) }
                files.forEach { file ->
                    addFormDataPart("files", file.fileName, file.asRequestBody())
                }
            }
            .build()

        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/chats/$chatId/attachments/album"))
                .post(multipart)
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            MessageDto.serializer()
        )
    }

    suspend fun acknowledgeDelivered(accessToken: String, chatId: String, messageId: String) =
        executeUnitRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/chats/$chatId/messages/delivered"))
                .post(
                    json.encodeToString(
                        UpdateMessageReceiptRequest.serializer(),
                        UpdateMessageReceiptRequest(messageId)
                    ).toRequestBody(JSON)
                )
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build()
        )

    suspend fun acknowledgeRead(accessToken: String, chatId: String, messageId: String) =
        executeUnitRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/chats/$chatId/messages/read"))
                .post(
                    json.encodeToString(
                        UpdateMessageReceiptRequest.serializer(),
                        UpdateMessageReceiptRequest(messageId)
                    ).toRequestBody(JSON)
                )
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build()
        )

    suspend fun setMessageReaction(accessToken: String, chatId: String, messageId: String, emoji: String): MessageDto =
        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/chats/$chatId/messages/$messageId/reaction"))
                .put(
                    json.encodeToString(
                        SetMessageReactionRequest.serializer(),
                        SetMessageReactionRequest(emoji = emoji)
                    ).toRequestBody(JSON)
                )
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            MessageDto.serializer()
        )

    suspend fun downloadAttachmentTo(
        accessToken: String,
        attachment: AttachmentDto,
        targetFile: File,
        onProgress: ((bytesRead: Long, totalBytes: Long) -> Unit)? = null
    ): DownloadedAttachmentFile =
        withContext(Dispatchers.IO) {
            val request = Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl(attachment.downloadPath))
                .get()
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build()

            try {
                httpClient.newCall(request).execute().use { response ->
                    if (!response.isSuccessful) {
                        throw ApiException(response.code, formatApiErrorMessage(response.code, response.body?.string().orEmpty()))
                    }

                    val body = response.body ?: throw ApiException(response.code, "Empty attachment response.")
                    val totalBytes = body.contentLength()
                        .takeIf { it >= 0L }
                        ?: attachment.fileSizeBytes.takeIf { it >= 0L }
                        ?: -1L
                    targetFile.parentFile?.mkdirs()
                    body.byteStream().use { input ->
                        targetFile.outputStream().use { output ->
                            val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
                            var bytesRead = 0L
                            while (true) {
                                val read = input.read(buffer)
                                if (read < 0) {
                                    break
                                }
                                output.write(buffer, 0, read)
                                bytesRead += read
                                onProgress?.invoke(bytesRead, totalBytes)
                            }
                        }
                    }

            DownloadedAttachmentFile(
                        fileName = attachment.fileName,
                        contentType = body.contentType()?.toString()
                            ?: response.header("Content-Type")
                            ?: attachment.contentType,
                        fileSizeBytes = targetFile.length(),
                        localPath = targetFile.absolutePath
                    )
                }
            } catch (error: IOException) {
                throw ApiException(-1, networkErrorMessage())
            }
        }

    suspend fun getStories(accessToken: String): List<StoryDto> =
        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/stories"))
                .get()
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            kotlinx.serialization.builtins.ListSerializer(StoryDto.serializer())
        )

    suspend fun getStory(accessToken: String, storyId: String): StoryDto =
        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/stories/$storyId"))
                .get()
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            StoryDto.serializer()
        )

    suspend fun createStory(
        accessToken: String,
        text: String,
        visibility: StoryVisibility,
        hasProtectedContent: Boolean,
        ttlSeconds: Int
    ): StoryDto =
        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/stories"))
                .post(
                    json.encodeToString(
                        CreateStoryRequest.serializer(),
                        CreateStoryRequest(
                            text = text.takeIf { it.isNotBlank() },
                            visibility = visibility,
                            hasProtectedContent = hasProtectedContent,
                            ttlSeconds = ttlSeconds
                        )
                    ).toRequestBody(JSON)
                )
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            StoryDto.serializer()
        )

    suspend fun createMediaStory(
        accessToken: String,
        text: String?,
        visibility: StoryVisibility,
        hasProtectedContent: Boolean,
        ttlSeconds: Int,
        files: List<UploadFileSource>
    ): StoryDto = withContext(Dispatchers.IO) {
        val multipart = MultipartBody.Builder()
            .setType(MultipartBody.FORM)
            .apply {
                text?.takeIf { it.isNotBlank() }?.let { addFormDataPart("text", it) }
                addFormDataPart("visibility", visibility.wireValue.toString())
                addFormDataPart("hasProtectedContent", hasProtectedContent.toString())
                addFormDataPart("ttlSeconds", ttlSeconds.toString())
                files.forEach { file ->
                    addFormDataPart("files", file.fileName, file.asRequestBody())
                }
            }
            .build()

        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/stories/media"))
                .post(multipart)
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            StoryDto.serializer()
        )
    }

    suspend fun deleteStory(accessToken: String, storyId: String) =
        executeUnitRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/stories/$storyId"))
                .delete()
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build()
        )

    suspend fun getStoryViews(accessToken: String, storyId: String): List<StoryViewDto> =
        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/stories/$storyId/views"))
                .get()
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            kotlinx.serialization.builtins.ListSerializer(StoryViewDto.serializer())
        )

    suspend fun getMe(accessToken: String): UserSummaryDto =
        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/users/me"))
                .get()
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            UserSummaryDto.serializer()
        )

    suspend fun getUser(accessToken: String, userId: String): UserSummaryDto =
        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/users/$userId"))
                .get()
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            UserSummaryDto.serializer()
        )

    suspend fun updateMyProfile(accessToken: String, displayName: String, about: String?): UserSummaryDto =
        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/users/me"))
                .put(
                    json.encodeToString(
                        UpdateMyProfileRequest.serializer(),
                        UpdateMyProfileRequest(displayName = displayName, about = about?.takeIf { it.isNotBlank() })
                    ).toRequestBody(JSON)
                )
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            UserSummaryDto.serializer()
        )

    suspend fun uploadMyAvatar(
        accessToken: String,
        file: UploadFileSource
    ): UserSummaryDto = withContext(Dispatchers.IO) {
        val multipart = MultipartBody.Builder()
            .setType(MultipartBody.FORM)
            .addFormDataPart("file", file.fileName, file.asRequestBody())
            .build()

        val request = Request.Builder()
            .url(ServerConfig.absoluteMessengerUrl("/api/users/me/avatar"))
            .post(multipart)
            .withBearer(accessToken)
            .withClientVersionHeaders()
            .build()

        executeJsonRequest(request, UserSummaryDto.serializer())
    }

    suspend fun deleteMyAvatar(accessToken: String): UserSummaryDto =
        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/users/me/avatar"))
                .delete()
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            UserSummaryDto.serializer()
        )

    suspend fun getMyPrivacySettings(accessToken: String): PrivacySettingsDto =
        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/users/me/privacy"))
                .get()
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            PrivacySettingsDto.serializer()
        )

    suspend fun updateMyPrivacySettings(accessToken: String, request: UpdatePrivacySettingsRequest): PrivacySettingsDto =
        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/users/me/privacy"))
                .put(
                    json.encodeToString(
                        UpdatePrivacySettingsRequest.serializer(),
                        request
                    ).toRequestBody(JSON)
                )
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            PrivacySettingsDto.serializer()
        )

    suspend fun getMySessions(accessToken: String): List<UserSessionDto> =
        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/users/me/sessions"))
                .get()
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            kotlinx.serialization.builtins.ListSerializer(UserSessionDto.serializer())
        )

    suspend fun revokeMySession(accessToken: String, sessionId: String) =
        executeUnitRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/users/me/sessions/$sessionId"))
                .delete()
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build()
        )

    suspend fun searchUsers(accessToken: String, query: String): List<UserSummaryDto> =
        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/users/search?q=${java.net.URLEncoder.encode(query, Charsets.UTF_8.name())}"))
                .get()
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            kotlinx.serialization.builtins.ListSerializer(UserSummaryDto.serializer())
        )

    suspend fun resolveContacts(accessToken: String, phoneNumbers: List<String>): List<ContactMatchDto> =
        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/users/contacts/resolve"))
                .post(
                    json.encodeToString(
                        ResolveContactsRequest.serializer(),
                        ResolveContactsRequest(phoneNumbers)
                    ).toRequestBody(JSON)
                )
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            kotlinx.serialization.builtins.ListSerializer(ContactMatchDto.serializer())
        )

    suspend fun createDirectChat(accessToken: String, userId: String): ChatDetailsDto =
        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/chats/direct"))
                .post(
                    json.encodeToString(
                        CreateDirectChatRequest.serializer(),
                        CreateDirectChatRequest(userId)
                    ).toRequestBody(JSON)
                )
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            ChatDetailsDto.serializer()
        )

    suspend fun createGroupChat(accessToken: String, title: String, memberIds: List<String>): ChatDetailsDto =
        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/chats/group"))
                .post(
                    json.encodeToString(
                        CreateGroupChatRequest.serializer(),
                        CreateGroupChatRequest(title, memberIds)
                    ).toRequestBody(JSON)
                )
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            ChatDetailsDto.serializer()
        )

    suspend fun createChannel(
        accessToken: String,
        title: String,
        memberIds: List<String>,
        description: String? = null,
        publicUsername: String? = null,
        createInviteLink: Boolean = false,
        channelSignaturesEnabled: Boolean = false
    ): ChatDetailsDto =
        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/chats/channel"))
                .post(
                    json.encodeToString(
                        CreateChannelRequest.serializer(),
                        CreateChannelRequest(
                            title = title,
                            memberIds = memberIds,
                            description = description?.takeIf { it.isNotBlank() },
                            publicUsername = publicUsername?.takeIf { it.isNotBlank() },
                            createInviteLink = createInviteLink,
                            channelSignaturesEnabled = channelSignaturesEnabled
                        )
                    ).toRequestBody(JSON)
                )
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            ChatDetailsDto.serializer()
        )

    suspend fun subscribeToChannel(accessToken: String, chatId: String): ChatDetailsDto =
        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/chats/$chatId/subscribe"))
                .post("".toRequestBody(null))
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            ChatDetailsDto.serializer()
        )

    suspend fun unsubscribeFromChannel(accessToken: String, chatId: String) =
        executeUnitRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/chats/$chatId/subscribe"))
                .delete()
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build()
        )

    suspend fun forwardMessage(
        accessToken: String,
        sourceChatId: String,
        messageId: String,
        targetChatId: String
    ): MessageDto =
        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/chats/$sourceChatId/messages/$messageId/forward"))
                .post(
                    json.encodeToString(
                        ForwardMessageRequest.serializer(),
                        ForwardMessageRequest(targetChatId)
                    ).toRequestBody(JSON)
                )
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            MessageDto.serializer()
        )

    suspend fun getMyBots(accessToken: String): List<BotSummaryDto> =
        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/bots/mine"))
                .get()
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            kotlinx.serialization.builtins.ListSerializer(BotSummaryDto.serializer())
        )

    suspend fun getBot(accessToken: String, botId: String): BotSummaryDto =
        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/bots/$botId"))
                .get()
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            BotSummaryDto.serializer()
        )

    suspend fun createBot(accessToken: String, username: String, displayName: String, about: String?): CreatedBotDto =
        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/bots"))
                .post(
                    json.encodeToString(
                        CreateBotRequest.serializer(),
                        CreateBotRequest(username, displayName, about?.takeIf { it.isNotBlank() })
                    ).toRequestBody(JSON)
                )
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            CreatedBotDto.serializer()
        )

    suspend fun updateBot(accessToken: String, botId: String, displayName: String, about: String?, isEnabled: Boolean): BotSummaryDto =
        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/bots/$botId"))
                .put(
                    json.encodeToString(
                        UpdateBotRequest.serializer(),
                        UpdateBotRequest(displayName, about?.takeIf { it.isNotBlank() }, isEnabled)
                    ).toRequestBody(JSON)
                )
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            BotSummaryDto.serializer()
        )

    suspend fun setBotCommands(accessToken: String, botId: String, commands: List<BotCommandDto>): BotSummaryDto =
        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/bots/$botId/commands"))
                .put(
                    json.encodeToString(
                        SetBotCommandsRequest.serializer(),
                        SetBotCommandsRequest(commands)
                    ).toRequestBody(JSON)
                )
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            BotSummaryDto.serializer()
        )

    suspend fun regenerateBotToken(accessToken: String, botId: String): RegenerateBotTokenResponseDto =
        executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/bots/$botId/regenerate-token"))
                .post(ByteArray(0).toRequestBody(null, 0, 0))
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build(),
            RegenerateBotTokenResponseDto.serializer()
        )

    suspend fun deleteBot(accessToken: String, botId: String) =
        executeUnitRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/bots/$botId"))
                .delete()
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build()
        )

    suspend fun registerPushDevice(accessToken: String, request: RegisterPushDeviceRequest) =
        executeUnitRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/push/devices"))
                .post(json.encodeToString(RegisterPushDeviceRequest.serializer(), request).toRequestBody(JSON))
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build()
        )

    suspend fun unregisterPushDevice(accessToken: String, installationId: String) =
        executeUnitRequest(
            Request.Builder()
                .url(ServerConfig.absoluteMessengerUrl("/api/push/devices/${installationId.trim()}"))
                .delete()
                .withBearer(accessToken)
                .withClientVersionHeaders()
                .build()
        )

    suspend fun getLatestAndroidRelease(): AndroidAppReleaseDto {
        val manifest = executeJsonRequest(
            Request.Builder()
                .url(ServerConfig.absoluteArgusUrl(ServerConfig.AndroidManifestPath))
                .get()
                .withClientVersionHeaders()
                .build(),
            ArgusManifestDto.serializer()
        )

        val version = manifest.release.version
        val plusIndex = version.lastIndexOf('+')
        val versionName = if (plusIndex > 0) version.substring(0, plusIndex) else version
        val versionCode = version.substringAfterLast('+', "0").toIntOrNull() ?: 0

        return AndroidAppReleaseDto(
            versionCode = versionCode,
            versionName = versionName,
            downloadPath = ServerConfig.absoluteArgusUrl(manifest.release.downloadPath),
            publishedAt = manifest.release.publishedAt,
            notes = manifest.release.releaseNotes,
            isRequired = true,
            minimumSupportedVersionCode = versionCode
        )
    }

    private suspend fun <T> executeJsonRequest(request: Request, serializer: kotlinx.serialization.KSerializer<T>): T =
        withContext(Dispatchers.IO) {
            try {
                httpClient.newCall(request).execute().use { response ->
                    val body = response.body?.string().orEmpty()
                    if (!response.isSuccessful) {
                        throw ApiException(response.code, formatApiErrorMessage(response.code, body))
                    }

                    return@withContext try {
                        json.decodeFromString(serializer, body)
                    } catch (error: SerializationException) {
                        throw ApiException(response.code, "Failed to decode server response: ${error.message}")
                    }
                }
            } catch (error: IOException) {
                throw ApiException(-1, networkErrorMessage())
            }
        }

    private suspend fun executeUnitRequest(request: Request) =
        withContext(Dispatchers.IO) {
            try {
                httpClient.newCall(request).execute().use { response ->
                    val body = response.body?.string().orEmpty()
                    if (!response.isSuccessful) {
                        throw ApiException(response.code, formatApiErrorMessage(response.code, body))
                    }
                }
            } catch (error: IOException) {
                throw ApiException(-1, networkErrorMessage())
            }
        }

    private fun formatApiErrorMessage(statusCode: Int, responseBody: String): String {
        val trimmedBody = responseBody.trim()
        val problemDetailsMessage = trimmedBody
            .takeIf { it.startsWith("{") && it.endsWith("}") }
            ?.let(::parseProblemDetailsMessage)
            ?.takeIf { it.isNotBlank() }

        return when {
            !problemDetailsMessage.isNullOrBlank() -> problemDetailsMessage
            trimmedBody.startsWith("{") && trimmedBody.endsWith("}") -> defaultApiErrorMessage(statusCode)
            trimmedBody.isNotBlank() -> trimmedBody
            else -> defaultApiErrorMessage(statusCode)
        }
    }

    private fun parseProblemDetailsMessage(responseBody: String): String? = runCatching {
        val jsonObject = json.parseToJsonElement(responseBody).jsonObject
        val detail = jsonObject["detail"]?.jsonPrimitive?.content?.trim().orEmpty()
        val title = jsonObject["title"]?.jsonPrimitive?.content?.trim().orEmpty()
        when {
            detail.isNotBlank() -> detail
            title.isNotBlank() && title != "Unauthorized" -> title
            else -> null
        }
    }.getOrNull()

    private fun defaultApiErrorMessage(statusCode: Int): String = when (statusCode) {
        400 -> "Некорректный запрос."
        401 -> "Ошибка авторизации."
        403 -> "Доступ запрещён."
        404 -> "Ничего не найдено."
        409 -> "Конфликт данных."
        429 -> "Слишком много запросов."
        in 500..599 -> "Ошибка сервера."
        else -> "HTTP $statusCode"
    }

    private fun networkErrorMessage(): String =
        "Не удалось подключиться к серверу. Проверьте интернет и попробуйте ещё раз."

    private fun Request.Builder.withBearer(accessToken: String): Request.Builder =
        header("Authorization", "Bearer $accessToken")

    private fun Request.Builder.withClientVersionHeaders(): Request.Builder =
        header("X-Ikar-Client-Platform", clientVersionProvider.platform)
            .header("X-Ikar-Client-Version-Code", clientVersionProvider.versionCode)
            .header("X-Ikar-Client-Version-Name", clientVersionProvider.versionName)

    private fun buildSearchPath(
        basePath: String,
        query: String,
        filter: String?,
        type: String?,
        page: Int,
        limit: Int,
        dateFrom: String?,
        dateTo: String?,
        author: String?
    ): String {
        val queryParts = mutableListOf(
            "q=${encodeQuery(query)}",
            "page=${page.coerceAtLeast(1)}",
            "limit=${limit.coerceIn(1, 100)}"
        )
        filter?.takeIf { it.isNotBlank() }?.let { queryParts += "filter=${encodeQuery(it)}" }
        type?.takeIf { it.isNotBlank() }?.let { queryParts += "type=${encodeQuery(it)}" }
        dateFrom?.takeIf { it.isNotBlank() }?.let { queryParts += "dateFrom=${encodeQuery(it)}" }
        dateTo?.takeIf { it.isNotBlank() }?.let { queryParts += "dateTo=${encodeQuery(it)}" }
        author?.takeIf { it.isNotBlank() }?.let { queryParts += "author=${encodeQuery(it)}" }
        return "$basePath?${queryParts.joinToString("&")}"
    }

    private fun encodeQuery(value: String): String =
        URLEncoder.encode(value, StandardCharsets.UTF_8.toString())

    companion object {
        private val JSON = "application/json; charset=utf-8".toMediaType()
    }
}

data class ClientVersionProvider(
    val platform: String = "android",
    val versionCode: String,
    val versionName: String
)
