package ru.forbion.f7cloud.feature.mail

import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONArray
import org.json.JSONObject
import ru.forbion.f7cloud.core.auth.AuthSession
import ru.forbion.f7cloud.core.network.NetworkFactory
import ru.forbion.f7cloud.core.network.UnauthorizedException

class MailRepository {
    suspend fun loadBootstrap(session: AuthSession): MailBootstrap {
        val client = authedClient(session)
        val base = apiBase(session)
        val accountsJson = getJsonArray(client, "$base/accounts")
        if (accountsJson.length() == 0) {
            return MailBootstrap(emptyList(), emptyList(), emptyList(), null)
        }
        val accounts = parseAccounts(accountsJson).distinctBy { it.email.lowercase() }
        val allMailboxes = mutableListOf<MailMailbox>()
        val allFolders = mutableListOf<MailFolderEntry>()
        for (account in accounts) {
            val mailboxes = parseMailboxes(getJson(client, "$base/mailboxes?accountId=${account.id}"))
            allMailboxes += mailboxes
            allFolders += buildFolderList(account, mailboxes)
        }
        val selected = allFolders.firstOrNull { it.specialRole == "inbox" && it.filter == MailListFilter.ALL }
            ?: allFolders.firstOrNull()
        return MailBootstrap(accounts, allMailboxes, allFolders, selected)
    }

    suspend fun loadMessages(
        session: AuthSession,
        folder: MailFolderEntry,
        searchQuery: String = "",
        searchParams: MailSearchParams = MailSearchParams(),
        cursor: Long? = null,
        limit: Int = PAGE_SIZE,
    ): MailMessagesPage {
        val client = authedClient(session)
        val base = apiBase(session)
        val filter = buildFilterQuery(folder.filter, searchQuery, searchParams)
        val urlBuilder = StringBuilder("$base/messages?mailboxId=${folder.mailboxId}&limit=$limit&view=singleton")
        if (filter.isNotBlank()) urlBuilder.append("&filter=").append(java.net.URLEncoder.encode(filter, "UTF-8"))
        if (cursor != null) urlBuilder.append("&cursor=$cursor")
        val messages = parseMessages(getJson(client, urlBuilder.toString()))
        return pageFromMessages(messages, limit)
    }

    internal fun pageFromMessages(messages: List<MailMessage>, limit: Int): MailMessagesPage {
        // API cursor is sent_at (dateInt, Unix-секунды) — Long, без обрезки в Int.
        val nextCursor = if (messages.size >= limit) {
            messages.lastOrNull()?.dateInt?.takeIf { it > 0 }
        } else {
            null
        }
        return MailMessagesPage(messages, nextCursor)
    }

    suspend fun loadMessage(session: AuthSession, messageId: Int): MailMessageDetail {
        val client = authedClient(session)
        val base = apiBase(session)
        val json = getJson(client, "$base/messages/$messageId/body") as JSONObject
        val body = json.optString("body")
        val hasHtml = json.optBoolean("hasHtmlBody", false)
        val flagsJson = json.optJSONObject("flags")
        return MailMessageDetail(
            id = json.optInt("databaseId", messageId),
            subject = json.optString("subject").ifBlank { "(без темы)" },
            from = parseAddressLabel(json.opt("from")),
            fromEmail = parseAddressEmail(json.opt("from")),
            to = parseAddressList(json.opt("to")),
            cc = parseAddressList(json.opt("cc")),
            dateInt = json.optLong("dateInt", 0L),
            bodyHtml = if (hasHtml) body else "",
            bodyPlain = if (hasHtml) "" else body,
            hasHtmlBody = hasHtml,
            flags = parseFlags(flagsJson),
            attachments = parseAttachments(json.optJSONArray("attachments"), session.serverUrl),
            tags = parseTags(json.optJSONObject("tags")),
        )
    }

    suspend fun loadThread(session: AuthSession, messageId: Int): List<MailMessage> {
        val client = authedClient(session)
        val base = apiBase(session)
        return runCatching {
            parseMessages(getJson(client, "$base/messages/$messageId/thread"))
        }.getOrDefault(emptyList())
    }

    suspend fun loadAppSettings(session: AuthSession): MailAppSettings {
        val client = authedClient(session)
        val base = apiBase(session)
        val showThreaded = loadPreference(client, base, "layout-message-view", "threaded") == "threaded"
        val highlightExternal = loadPreference(client, base, "internal-addresses", "false") == "true"
        val allowNewAccounts = loadPreference(client, base, "allow-new-accounts", "true") != "false"
        val trustedSenders = loadTrustedSenders(client, base)
        val textBlocks = loadTextBlocks(client, base)
        return MailAppSettings(
            showThreaded = showThreaded,
            highlightExternalAddresses = highlightExternal,
            allowNewAccounts = allowNewAccounts,
            trustedSenders = trustedSenders,
            textBlocks = textBlocks,
        )
    }

    suspend fun savePreference(session: AuthSession, key: String, value: String): String {
        val client = authedClient(session)
        val payload = JSONObject().put("value", value).toString()
        val request = Request.Builder()
            .url("${apiBase(session)}/preferences/${java.net.URLEncoder.encode(key, "UTF-8")}")
            .header("OCS-APIRequest", "true")
            .header("Accept", "application/json")
            .header("Content-Type", "application/json")
            .put(payload.toRequestBody("application/json".toMediaType()))
            .build()
        client.newCall(request).execute().use { response ->
            if (!response.isSuccessful || response.body == null) {
                error("Mail API HTTP ${response.code}")
            }
            val json = JSONObject(response.body!!.string())
            return json.optString("value", value)
        }
    }

    suspend fun removeTrustedSender(session: AuthSession, email: String, type: String) {
        val client = authedClient(session)
        val encodedEmail = java.net.URLEncoder.encode(email, "UTF-8")
        val encodedType = java.net.URLEncoder.encode(type, "UTF-8")
        val request = Request.Builder()
            .url("${apiBase(session)}/trustedsenders/$encodedEmail?type=$encodedType")
            .header("OCS-APIRequest", "true")
            .header("Accept", "application/json")
            .delete()
            .build()
        client.newCall(request).execute().use { response ->
            if (!response.isSuccessful) error("Mail API HTTP ${response.code}")
        }
    }

    suspend fun createTextBlock(session: AuthSession, title: String, content: String): MailTextBlock {
        val client = authedClient(session)
        val payload = JSONObject()
            .put("title", title)
            .put("content", content)
            .toString()
        val request = Request.Builder()
            .url("${apiBase(session)}/textBlocks")
            .header("OCS-APIRequest", "true")
            .header("Accept", "application/json")
            .header("Content-Type", "application/json")
            .post(payload.toRequestBody("application/json".toMediaType()))
            .build()
        client.newCall(request).execute().use { response ->
            if (!response.isSuccessful || response.body == null) {
                error("Mail API HTTP ${response.code}")
            }
            val json = unwrapData(JSONObject(response.body!!.string()))
            return parseTextBlock(json as JSONObject)
        }
    }

    suspend fun deleteTextBlock(session: AuthSession, id: Int) {
        val client = authedClient(session)
        val request = Request.Builder()
            .url("${apiBase(session)}/textBlocks/$id")
            .header("OCS-APIRequest", "true")
            .header("Accept", "application/json")
            .delete()
            .build()
        client.newCall(request).execute().use { response ->
            if (!response.isSuccessful) error("Mail API HTTP ${response.code}")
        }
    }

    suspend fun loadAccountSettings(session: AuthSession, accountId: Int): MailAccountSettings {
        val client = authedClient(session)
        val json = getJson(client, "${apiBase(session)}/accounts/$accountId") as JSONObject
        return parseAccountSettings(json)
    }

    suspend fun patchAccountSettings(
        session: AuthSession,
        accountId: Int,
        draftsMailboxId: Int? = null,
        sentMailboxId: Int? = null,
        trashMailboxId: Int? = null,
        archiveMailboxId: Int? = null,
        junkMailboxId: Int? = null,
        searchBody: Boolean? = null,
        classificationEnabled: Boolean? = null,
        signatureAboveQuote: Boolean? = null,
        imipCreate: Boolean? = null,
    ): MailAccountSettings {
        val payload = JSONObject()
        draftsMailboxId?.let { payload.put("draftsMailboxId", it) }
        sentMailboxId?.let { payload.put("sentMailboxId", it) }
        trashMailboxId?.let { payload.put("trashMailboxId", it) }
        archiveMailboxId?.let { payload.put("archiveMailboxId", it) }
        junkMailboxId?.let { payload.put("junkMailboxId", it) }
        searchBody?.let { payload.put("searchBody", it) }
        classificationEnabled?.let { payload.put("classificationEnabled", it) }
        signatureAboveQuote?.let { payload.put("signatureAboveQuote", it) }
        imipCreate?.let { payload.put("imipCreate", it) }
        val client = authedClient(session)
        val request = Request.Builder()
            .url("${apiBase(session)}/accounts/$accountId")
            .header("OCS-APIRequest", "true")
            .header("Accept", "application/json")
            .header("Content-Type", "application/json")
            .patch(payload.toString().toRequestBody("application/json".toMediaType()))
            .build()
        client.newCall(request).execute().use { response ->
            if (!response.isSuccessful || response.body == null) {
                error("Mail API HTTP ${response.code}")
            }
            val body = response.body!!.string()
            return parseAccountSettings(JSONObject(body))
        }
    }

    suspend fun updateAccountSignature(session: AuthSession, accountId: Int, signature: String) {
        val payload = JSONObject().put("signature", signature).toString()
        val client = authedClient(session)
        val request = Request.Builder()
            .url("${apiBase(session)}/accounts/$accountId/signature")
            .header("OCS-APIRequest", "true")
            .header("Accept", "application/json")
            .header("Content-Type", "application/json")
            .put(payload.toRequestBody("application/json".toMediaType()))
            .build()
        client.newCall(request).execute().use { response ->
            if (!response.isSuccessful) error("Mail API HTTP ${response.code}")
        }
    }

    suspend fun loadOutboxMessages(session: AuthSession): List<MailOutboxMessage> {
        val client = authedClient(session)
        val json = getJson(client, "${apiBase(session)}/outbox")
        return parseOutboxMessages(json)
    }

    suspend fun sendOutboxMessage(session: AuthSession, messageId: Int) {
        val client = authedClient(session)
        val request = Request.Builder()
            .url("${apiBase(session)}/outbox/$messageId")
            .header("OCS-APIRequest", "true")
            .header("Accept", "application/json")
            .post("".toRequestBody("application/json".toMediaType()))
            .build()
        client.newCall(request).execute().use { response ->
            if (!response.isSuccessful) error("Не удалось отправить (HTTP ${response.code})")
        }
    }

    suspend fun deleteOutboxMessage(session: AuthSession, messageId: Int) {
        val client = authedClient(session)
        val request = Request.Builder()
            .url("${apiBase(session)}/outbox/$messageId")
            .header("OCS-APIRequest", "true")
            .header("Accept", "application/json")
            .delete()
            .build()
        client.newCall(request).execute().use { response ->
            if (!response.isSuccessful) error("Не удалось удалить (HTTP ${response.code})")
        }
    }

    suspend fun setMessageFlags(
        session: AuthSession,
        messageId: Int,
        seen: Boolean? = null,
        flagged: Boolean? = null,
        important: Boolean? = null,
    ) {
        val flags = JSONObject()
        seen?.let { flags.put("seen", it) }
        flagged?.let { flags.put("flagged", it) }
        important?.let { flags.put("important", it) }
        if (flags.length() == 0) return
        val body = JSONObject().put("flags", flags).toString()
        val client = authedClient(session)
        val request = Request.Builder()
            .url("${apiBase(session)}/messages/$messageId/flags")
            .header("OCS-APIRequest", "true")
            .header("Accept", "application/json")
            .put(body.toRequestBody("application/json".toMediaType()))
            .build()
        client.newCall(request).execute().use { response ->
            if (!response.isSuccessful) error("Mail API HTTP ${response.code}")
        }
    }

    suspend fun moveMessage(session: AuthSession, messageId: Int, destMailboxId: Int) {
        val client = authedClient(session)
        val url = "${apiBase(session)}/messages/$messageId/move?destFolderId=$destMailboxId"
        val request = Request.Builder()
            .url(url)
            .header("OCS-APIRequest", "true")
            .header("Accept", "application/json")
            .post("".toRequestBody("application/json".toMediaType()))
            .build()
        client.newCall(request).execute().use { response ->
            if (!response.isSuccessful) error("Не удалось переместить (HTTP ${response.code})")
        }
    }

    suspend fun snoozeMessage(
        session: AuthSession,
        messageId: Int,
        unixTimestamp: Long,
        destMailboxId: Int,
    ) {
        val client = authedClient(session)
        val url = "${apiBase(session)}/messages/$messageId/snooze" +
            "?unixTimestamp=$unixTimestamp&destMailboxId=$destMailboxId"
        val request = Request.Builder()
            .url(url)
            .header("OCS-APIRequest", "true")
            .header("Accept", "application/json")
            .post("".toRequestBody("application/json".toMediaType()))
            .build()
        client.newCall(request).execute().use { response ->
            if (!response.isSuccessful) error("Не удалось отложить (HTTP ${response.code})")
        }
    }

    suspend fun setMessageTag(session: AuthSession, messageId: Int, imapLabel: String, add: Boolean) {
        val client = authedClient(session)
        val encoded = java.net.URLEncoder.encode(imapLabel, Charsets.UTF_8.name())
        val request = Request.Builder()
            .url("${apiBase(session)}/messages/$messageId/tags/$encoded")
            .header("OCS-APIRequest", "true")
            .header("Accept", "application/json")
            .let { builder ->
                if (add) {
                    builder.put("".toRequestBody("application/json".toMediaType()))
                } else {
                    builder.delete()
                }
            }
            .build()
        client.newCall(request).execute().use { response ->
            if (!response.isSuccessful) error("Не удалось изменить метку (HTTP ${response.code})")
        }
    }

    suspend fun deleteMessage(session: AuthSession, messageId: Int) {
        val client = authedClient(session)
        val request = Request.Builder()
            .url("${apiBase(session)}/messages/$messageId")
            .header("OCS-APIRequest", "true")
            .delete()
            .build()
        client.newCall(request).execute().use { response ->
            if (!response.isSuccessful) error("Mail API HTTP ${response.code}")
        }
    }

    suspend fun downloadAttachment(
        session: AuthSession,
        messageId: Int,
        attachmentId: String,
    ): ByteArray {
        val client = authedClient(session)
        val request = Request.Builder()
            .url("${apiBase(session)}/messages/$messageId/attachment/$attachmentId")
            .header("OCS-APIRequest", "true")
            .build()
        client.newCall(request).execute().use { response ->
            if (!response.isSuccessful || response.body == null) {
                error("Вложение HTTP ${response.code}")
            }
            return response.body!!.bytes()
        }
    }

    fun composeUrl(session: AuthSession): String {
        return "${session.serverUrl.trimEnd('/')}/index.php/apps/mail/compose"
    }

    suspend fun uploadLocalAttachment(
        session: AuthSession,
        fileName: String,
        bytes: ByteArray,
        mimeType: String,
    ): MailComposeAttachment {
        val client = authedClient(session)
        val mediaType = mimeType.toMediaTypeOrNull() ?: "application/octet-stream".toMediaType()
        val body = MultipartBody.Builder()
            .setType(MultipartBody.FORM)
            .addFormDataPart(
                "attachment",
                fileName,
                bytes.toRequestBody(mediaType),
            )
            .build()
        val request = Request.Builder()
            .url("${apiBase(session)}/attachments")
            .header("OCS-APIRequest", "true")
            .header("Accept", "application/json")
            .post(body)
            .build()
        client.newCall(request).execute().use { response ->
            if (!response.isSuccessful || response.body == null) {
                error("Не удалось загрузить вложение (HTTP ${response.code})")
            }
            val json = JSONObject(response.body!!.string())
            val data = json.optJSONObject("data") ?: json
            val id = data.optInt("id", 0)
            if (id <= 0) error("Не удалось загрузить вложение")
            return MailComposeAttachment(
                id = id,
                fileName = data.optString("fileName", fileName),
                mimeType = data.optString("mimeType", mimeType),
            )
        }
    }

    suspend fun createAndSendMessage(
        session: AuthSession,
        accountId: Int,
        to: List<MailRecipient>,
        cc: List<MailRecipient>,
        bcc: List<MailRecipient>,
        subject: String,
        bodyHtml: String,
        bodyPlain: String,
        editorBody: String,
        requestMdn: Boolean,
        sendAt: Int?,
        attachments: List<MailComposeAttachment> = emptyList(),
    ) {
        val client = authedClient(session)
        val payload = JSONObject().apply {
            put("accountId", accountId)
            put("subject", subject)
            put("bodyPlain", bodyPlain)
            put("bodyHtml", bodyHtml)
            put("editorBody", editorBody)
            put("isHtml", true)
            put("smimeSign", false)
            put("smimeEncrypt", false)
            put("requestMdn", requestMdn)
            put("isPgpMime", false)
            put("to", recipientsJson(to))
            put("cc", recipientsJson(cc))
            put("bcc", recipientsJson(bcc))
            put("attachments", attachmentsJson(attachments))
            if (sendAt != null) put("sendAt", sendAt)
        }
        val createRequest = Request.Builder()
            .url("${apiBase(session)}/outbox")
            .header("OCS-APIRequest", "true")
            .header("Accept", "application/json")
            .header("Content-Type", "application/json")
            .post(payload.toString().toRequestBody("application/json".toMediaType()))
            .build()
        val messageId = client.newCall(createRequest).execute().use { response ->
            if (!response.isSuccessful || response.body == null) {
                error("Не удалось создать письмо (HTTP ${response.code})")
            }
            val body = response.body!!.string()
            val json = JSONObject(body)
            val data = json.optJSONObject("data") ?: json
            data.optInt("id", data.optInt("databaseId", 0))
        }
        if (messageId <= 0) error("Не удалось создать письмо")
        val sendRequest = Request.Builder()
            .url("${apiBase(session)}/outbox/$messageId")
            .header("OCS-APIRequest", "true")
            .header("Accept", "application/json")
            .post("".toRequestBody("application/json".toMediaType()))
            .build()
        client.newCall(sendRequest).execute().use { response ->
            if (!response.isSuccessful) {
                error("Не удалось отправить письмо (HTTP ${response.code})")
            }
        }
    }

    private fun attachmentsJson(attachments: List<MailComposeAttachment>): JSONArray {
        val array = JSONArray()
        attachments.forEach { attachment ->
            array.put(
                when (attachment.type) {
                    MailComposeAttachmentType.LOCAL -> JSONObject().apply {
                        put("type", "local")
                        put("id", attachment.id)
                    }
                    MailComposeAttachmentType.CLOUD -> JSONObject().apply {
                        put("type", "cloud")
                        put(
                            "fileName",
                            attachment.cloudPath ?: "/${attachment.fileName.trim('/')}",
                        )
                        attachment.size?.let { put("size", it) }
                    }
                },
            )
        }
        return array
    }

    private fun recipientsJson(recipients: List<MailRecipient>): JSONArray {
        val array = JSONArray()
        recipients.forEach { recipient ->
            array.put(
                JSONObject().apply {
                    put("email", recipient.email)
                    put("label", recipient.label.ifBlank { recipient.email })
                },
            )
        }
        return array
    }

    fun parseRecipients(raw: String): List<MailRecipient> {
        if (raw.isBlank()) return emptyList()
        return raw.split(',', ';')
            .map { it.trim() }
            .filter { it.isNotBlank() }
            .map { token ->
                val match = Regex("<([^>]+)>").find(token)
                if (match != null) {
                    val email = match.groupValues[1].trim()
                    val label = token.replace(match.value, "").trim().trim('"')
                    MailRecipient(email = email, label = label.ifBlank { email })
                } else {
                    MailRecipient(email = token, label = token)
                }
            }
    }

    fun replyUrl(session: AuthSession, mailboxId: Int, messageId: Int): String {
        return "${session.serverUrl.trimEnd('/')}/index.php/apps/mail/box/$mailboxId/thread/$messageId"
    }

    private fun loadPreference(client: OkHttpClient, base: String, key: String, default: String): String {
        val json = getJson(client, "$base/preferences/${java.net.URLEncoder.encode(key, "UTF-8")}") as JSONObject
        return json.optString("value", default)
    }

    private fun loadTrustedSenders(client: OkHttpClient, base: String): List<MailTrustedSender> {
        return runCatching {
            val data = unwrapData(getJson(client, "$base/trustedsenders"))
            parseTrustedSenders(data)
        }.getOrDefault(emptyList())
    }

    private fun loadTextBlocks(client: OkHttpClient, base: String): List<MailTextBlock> {
        return runCatching {
            val data = unwrapData(getJson(client, "$base/textBlocks"))
            parseTextBlocks(data)
        }.getOrDefault(emptyList())
    }

    private fun unwrapData(json: Any): Any {
        if (json is JSONObject && json.optString("status") == "success") {
            return json.opt("data") ?: json
        }
        return json
    }

    private fun parseTrustedSenders(data: Any): List<MailTrustedSender> {
        val array = when (data) {
            is JSONArray -> data
            is JSONObject -> data.optJSONArray("data") ?: JSONArray()
            else -> JSONArray()
        }
        return buildList {
            for (i in 0 until array.length()) {
                val obj = array.optJSONObject(i) ?: continue
                val email = obj.optString("email")
                if (email.isBlank()) continue
                add(
                    MailTrustedSender(
                        id = obj.optInt("id"),
                        email = email,
                        type = obj.optString("type", "individual"),
                    ),
                )
            }
        }.sortedBy { it.email.lowercase() }
    }

    private fun parseTextBlocks(data: Any): List<MailTextBlock> {
        val array = when (data) {
            is JSONArray -> data
            is JSONObject -> data.optJSONArray("data") ?: JSONArray()
            else -> JSONArray()
        }
        return buildList {
            for (i in 0 until array.length()) {
                val obj = array.optJSONObject(i) ?: continue
                val id = obj.optInt("id")
                if (id <= 0) continue
                add(parseTextBlock(obj))
            }
        }.sortedBy { it.title.lowercase() }
    }

    private fun parseTextBlock(obj: JSONObject): MailTextBlock {
        return MailTextBlock(
            id = obj.optInt("id"),
            title = obj.optString("title"),
            content = obj.optString("content"),
            preview = obj.optString("preview"),
        )
    }

    private fun authedClient(session: AuthSession): OkHttpClient {
        return NetworkFactory.newAuthedClient(
            session.username,
            session.appPassword,
            session.trustAllCerts,
        )
    }

    private fun apiBase(session: AuthSession): String {
        return "${session.serverUrl.trimEnd('/')}/index.php/apps/mail/api"
    }

    private fun buildFilterQuery(
        filter: MailListFilter,
        search: String,
        searchParams: MailSearchParams = MailSearchParams(),
    ): String {
        val parts = mutableListOf<String>()
        when (filter) {
            MailListFilter.UNREAD -> parts += "is:unread"
            MailListFilter.STARRED -> parts += "is:starred"
            MailListFilter.ALL -> Unit
        }
        val advanced = searchParams.toFilterString()
        if (advanced.isNotBlank()) parts += advanced
        if (search.isNotBlank()) parts += search.trim()
        return parts.joinToString(" ")
    }

    private fun buildFolderList(account: MailAccount, mailboxes: List<MailMailbox>): List<MailFolderEntry> {
        val byId = mailboxes.associateBy { it.id }
        val result = mutableListOf<MailFolderEntry>()
        fun add(mailbox: MailMailbox?, title: String, role: String?, filter: MailListFilter = MailListFilter.ALL) {
            if (mailbox == null) return
            result += MailFolderEntry(
                mailboxId = mailbox.id,
                accountId = account.id,
                title = title,
                specialRole = role,
                filter = filter,
                unread = mailbox.unread,
            )
        }
        fun mailboxForRole(role: String, configuredId: Int?): MailMailbox? {
            mailboxes.firstOrNull { it.specialRole == role }?.let { return it }
            val configured = configuredId?.let { byId[it] }
            if (configured != null && (role != "sent" || configured.specialRole != "inbox" && !configured.isInbox)) {
                return configured
            }
            return null
        }
        val inbox = mailboxes.firstOrNull { it.isInbox || it.specialRole == "inbox" }
        add(inbox, "Входящие", "inbox")
        if (inbox != null) {
            add(inbox, "Непрочитанные", "inbox", MailListFilter.UNREAD)
            add(inbox, "Избранное", "inbox", MailListFilter.STARRED)
        }
        add(mailboxForRole("sent", account.sentMailboxId), "Отправленные", "sent")
        add(mailboxForRole("drafts", account.draftsMailboxId), "Черновики", "drafts")
        add(mailboxForRole("archive", account.archiveMailboxId), "Архив", "archive")
        add(mailboxForRole("trash", account.trashMailboxId), "Корзина", "trash")
        add(mailboxForRole("junk", account.junkMailboxId), "Спам", "junk")
        val usedIds = result.map { it.mailboxId }.toSet()
        mailboxes.filter { it.id !in usedIds && !it.isInbox && it.specialRole.isNullOrBlank() }
            .sortedBy { it.displayName.lowercase() }
            .forEach { add(it, it.displayName, null) }
        return result
    }

    private fun parseAccounts(array: JSONArray): List<MailAccount> {
        return buildList {
            for (i in 0 until array.length()) {
                val obj = array.optJSONObject(i) ?: continue
                val id = obj.optInt("accountId", obj.optInt("id", 0))
                if (id <= 0) continue
                add(
                    MailAccount(
                        id = id,
                        email = obj.optString("emailAddress").ifBlank { obj.optString("email") },
                        name = obj.optString("name"),
                        draftsMailboxId = obj.optInt("draftsMailboxId").takeIf { it > 0 },
                        sentMailboxId = obj.optInt("sentMailboxId").takeIf { it > 0 },
                        trashMailboxId = obj.optInt("trashMailboxId").takeIf { it > 0 },
                        archiveMailboxId = obj.optInt("archiveMailboxId").takeIf { it > 0 },
                        junkMailboxId = obj.optInt("junkMailboxId").takeIf { it > 0 },
                        snoozeMailboxId = obj.optInt("snoozeMailboxId").takeIf { it > 0 },
                    ),
                )
            }
        }
    }

    private fun getJsonArray(client: OkHttpClient, url: String): JSONArray {
        return when (val json = getJson(client, url)) {
            is JSONArray -> json
            else -> JSONArray()
        }
    }

    private fun getJson(client: OkHttpClient, url: String): Any {
        val request = Request.Builder()
            .url(url)
            .header("OCS-APIRequest", "true")
            .header("Accept", "application/json")
            .build()
        client.newCall(request).execute().use { response ->
            if (response.code == 412) error("Mail API: CSRF — обновите приложение")
            if (!response.isSuccessful || response.body == null) {
                error("Mail API HTTP ${response.code}")
            }
            val body = response.body!!.string().trim()
            if (body.startsWith("<?xml", ignoreCase = true)) {
                error("Mail API вернул XML — проверьте, что «Почта» включена на сервере")
            }
            if (body.startsWith("[")) return JSONArray(body)
            if (body.startsWith("{")) return JSONObject(body)
            error("Mail API: неожиданный ответ")
        }
    }

    private fun parseMailboxes(data: Any): List<MailMailbox> {
        val out = mutableListOf<MailMailbox>()
        fun addFromObject(obj: JSONObject) {
            val id = obj.optInt("databaseId", obj.optInt("id", 0))
            if (id <= 0) return
            val name = obj.optString("name")
            val displayName = obj.optString("displayName").ifBlank { name }
            val special = obj.optJSONArray("specialUse")
            var specialRole = obj.optString("specialRole").ifBlank { null }
            if (specialRole == null && special != null && special.length() > 0) {
                specialRole = special.optString(0).removePrefix("\\").lowercase()
            }
            var isInbox = specialRole == "inbox"
            if (!isInbox && special != null) {
                for (i in 0 until special.length()) {
                    if (special.optString(i).contains("inbox", ignoreCase = true)) {
                        isInbox = true
                        specialRole = "inbox"
                        break
                    }
                }
            }
            if (!isInbox && name.equals("INBOX", ignoreCase = true)) {
                isInbox = true
                specialRole = "inbox"
            }
            out += MailMailbox(
                id = id,
                accountId = obj.optInt("accountId"),
                displayName = displayName.ifBlank { "Mailbox" },
                name = name,
                specialRole = specialRole,
                unread = obj.optInt("unread", 0),
                isInbox = isInbox,
            )
            obj.optJSONArray("mailboxes")?.let { nested ->
                for (i in 0 until nested.length()) {
                    nested.optJSONObject(i)?.let { addFromObject(it) }
                }
            }
        }
        when (data) {
            is JSONArray -> for (i in 0 until data.length()) {
                data.optJSONObject(i)?.let { addFromObject(it) }
            }
            is JSONObject -> {
                val mailboxes = data.optJSONArray("mailboxes")
                if (mailboxes != null) {
                    for (i in 0 until mailboxes.length()) {
                        mailboxes.optJSONObject(i)?.let { addFromObject(it) }
                    }
                } else if (data.has("databaseId")) {
                    addFromObject(data)
                }
            }
        }
        return out
    }

    private fun parseMessages(data: Any): List<MailMessage> {
        val out = mutableListOf<MailMessage>()
        fun addFromObject(obj: JSONObject) {
            val id = obj.optInt("databaseId", obj.optInt("uid", 0))
            if (id <= 0) return
            val fromLabel = parseAddressLabel(obj.opt("from"))
            val fromEmail = parseAddressEmail(obj.opt("from"))
            out += MailMessage(
                id = id,
                subject = obj.optString("subject").ifBlank { "(без темы)" },
                from = fromLabel.ifBlank { "Unknown" },
                fromEmail = fromEmail,
                preview = obj.optString("previewText").ifBlank { obj.optString("summary") },
                dateInt = obj.optLong("dateInt", 0L),
                flags = parseFlags(obj.optJSONObject("flags")),
                tags = parseTags(obj.optJSONObject("tags")),
            )
        }
        when (data) {
            is JSONArray -> for (i in 0 until data.length()) {
                data.optJSONObject(i)?.let { addFromObject(it) }
            }
            is JSONObject -> {
                val keys = data.keys()
                while (keys.hasNext()) {
                    data.optJSONObject(keys.next())?.let { addFromObject(it) }
                }
            }
        }
        return out.sortedByDescending { it.dateInt }
    }

    private fun parseTags(tags: JSONObject?): List<MailTag> {
        if (tags == null) return emptyList()
        return buildList {
            val keys = tags.keys()
            while (keys.hasNext()) {
                val imapLabel = keys.next()
                val tagObj = tags.optJSONObject(imapLabel) ?: continue
                val name = tagObj.optString("displayName").trim()
                if (name.isEmpty()) continue
                add(
                    MailTag(
                        id = tagObj.optLong("id"),
                        displayName = name,
                        colorHex = tagObj.optString("color"),
                        imapLabel = imapLabel,
                    ),
                )
            }
        }
    }

    private fun parseFlags(flags: JSONObject?): MailMessageFlags {
        return MailMessageFlags(
            seen = flags?.optBoolean("seen", true) ?: true,
            flagged = flags?.optBoolean("flagged", false) ?: false,
            hasAttachments = flags?.optBoolean("hasAttachments", false) ?: false,
            answered = flags?.optBoolean("answered", false) ?: false,
            important = flags?.optBoolean("important", false) ?: false,
        )
    }

    private fun parseAttachments(array: JSONArray?, serverUrl: String): List<MailAttachment> {
        if (array == null) return emptyList()
        return buildList {
            for (i in 0 until array.length()) {
                val obj = array.optJSONObject(i) ?: continue
                val id = obj.optString("id").ifBlank { obj.optInt("id", 0).toString() }
                if (id == "0") continue
                add(
                    MailAttachment(
                        id = id,
                        fileName = obj.optString("fileName").ifBlank { "attachment" },
                        mime = obj.optString("mime").ifBlank { "application/octet-stream" },
                        size = obj.optLong("size", 0L),
                        cid = obj.optString("cid").ifBlank { null },
                        downloadUrl = obj.optString("downloadUrl").ifBlank { null },
                    ),
                )
            }
        }
    }

    private fun parseAddressEmail(value: Any?): String {
        when (value) {
            is JSONObject -> return value.optString("email")
            is JSONArray -> return value.optJSONObject(0)?.optString("email").orEmpty()
        }
        return ""
    }

    private fun parseAddressLabel(value: Any?): String {
        when (value) {
            is JSONObject -> return value.optString("label").ifBlank { value.optString("email") }
            is JSONArray -> {
                if (value.length() == 0) return ""
                val first = value.optJSONObject(0) ?: return ""
                return first.optString("label").ifBlank { first.optString("email") }
            }
        }
        return ""
    }

    private fun parseAddressList(value: Any?): String {
        if (value !is JSONArray) return ""
        return buildList {
            for (i in 0 until value.length()) {
                val label = parseAddressLabel(value.opt(i))
                if (label.isNotBlank()) add(label)
            }
        }.joinToString(", ")
    }

    private fun parseAccountSettings(json: JSONObject): MailAccountSettings {
        val data = json.optJSONObject("data") ?: json
        return MailAccountSettings(
            id = data.optInt("accountId", data.optInt("id")),
            email = data.optString("emailAddress").ifBlank { data.optString("email") },
            name = data.optString("name"),
            signature = data.optString("signature"),
            draftsMailboxId = data.optInt("draftsMailboxId").takeIf { it > 0 },
            sentMailboxId = data.optInt("sentMailboxId").takeIf { it > 0 },
            trashMailboxId = data.optInt("trashMailboxId").takeIf { it > 0 },
            archiveMailboxId = data.optInt("archiveMailboxId").takeIf { it > 0 },
            junkMailboxId = data.optInt("junkMailboxId").takeIf { it > 0 },
            searchBody = data.optBoolean("searchBody", false),
            classificationEnabled = data.optBoolean("classificationEnabled", false),
            signatureAboveQuote = data.optBoolean("signatureAboveQuote", false),
            imipCreate = data.optBoolean("imipCreate", false),
            quotaPercentage = data.optInt("quotaPercentage").takeIf { it > 0 },
            imapHost = data.optString("imapHost").ifBlank { null },
            smtpHost = data.optString("smtpHost").ifBlank { null },
        )
    }

    private fun parseOutboxMessages(data: Any): List<MailOutboxMessage> {
        val array = when (data) {
            is JSONObject -> {
                val root = data.optJSONObject("data") ?: data
                root.optJSONArray("messages") ?: JSONArray()
            }
            is JSONArray -> data
            else -> JSONArray()
        }
        return buildList {
            for (i in 0 until array.length()) {
                val obj = array.optJSONObject(i) ?: continue
                val id = obj.optInt("id")
                if (id <= 0) continue
                if (obj.optInt("status") == 12) continue
                val preview = obj.optString("bodyPlain")
                    .ifBlank { obj.optString("editorBody") }
                    .ifBlank { obj.optString("bodyHtml") }
                    .replace(Regex("<[^>]+>"), " ")
                    .trim()
                add(
                    MailOutboxMessage(
                        id = id,
                        accountId = obj.optInt("accountId"),
                        subject = obj.optString("subject").ifBlank { "(без темы)" },
                        toRecipients = parseAddressList(obj.opt("to")),
                        preview = preview,
                        updatedAt = obj.optLong("updatedAt"),
                        sendAt = obj.optLong("sendAt").takeIf { it > 0 },
                        failed = obj.optBoolean("failed"),
                        status = obj.optInt("status"),
                    ),
                )
            }
        }.sortedByDescending { it.updatedAt }
    }

    // --- Фильтры почты: официальный API NC Mail (тот же, что у веб-модалки «Новый фильтр») ---
    // GET/PUT /apps/mail/api/filter/{accountId}; сервер сам сериализует в sieve-скрипт
    // (секция «### Nextcloud Mail: Filters ###»), правила ОБЩИЕ между вебом и приложением.

    fun getMailFilters(session: AuthSession, accountId: Int): List<MailFilterRule> {
        val client = authedClient(session)
        val array = getJsonArray(client, "${apiBase(session)}/filter/$accountId")
        return buildList {
            for (i in 0 until array.length()) {
                array.optJSONObject(i)?.let { add(MailFilterRule(it)) }
            }
        }
    }

    fun putMailFilters(session: AuthSession, accountId: Int, filters: List<MailFilterRule>) {
        val client = authedClient(session)
        val payload = JSONObject()
            .put("filters", JSONArray().apply { filters.forEach { put(it.raw) } })
            .toString()
        val request = Request.Builder()
            .url("${apiBase(session)}/filter/$accountId")
            .header("OCS-APIRequest", "true")
            .header("Accept", "application/json")
            .put(payload.toRequestBody("application/json".toMediaType()))
            .build()
        client.newCall(request).execute().use { response ->
            if (!response.isSuccessful) {
                error("Filter API HTTP ${response.code}")
            }
        }
    }

    companion object {
        const val PAGE_SIZE = 20
    }
}

/**
 * Правило фильтра почты. Держим исходный JSON (raw) целиком, чтобы при правке
 * простых полей не терять незнакомые поля сложных правил, созданных в вебе.
 */
class MailFilterRule(val raw: JSONObject) {
    val name: String get() = raw.optString("name")
    val enable: Boolean get() = raw.optBoolean("enable", true)
    val operator: String get() = raw.optString("operator", "allof")
    val priority: Int get() = raw.optInt("priority", 10)

    val tests: List<Triple<String, String, List<String>>>
        get() = raw.optJSONArray("tests")?.let { arr ->
            buildList {
                for (i in 0 until arr.length()) {
                    val t = arr.optJSONObject(i) ?: continue
                    val values = t.optJSONArray("values")?.let { v ->
                        (0 until v.length()).map { v.optString(it) }
                    }.orEmpty()
                    add(Triple(t.optString("field"), t.optString("operator", "contains"), values))
                }
            }
        }.orEmpty()

    val actions: List<JSONObject>
        get() = raw.optJSONArray("actions")?.let { arr ->
            (0 until arr.length()).mapNotNull { arr.optJSONObject(it) }
        }.orEmpty()

    /** Простое правило (1 тест, действия из нашего конструктора) — редактируемо инлайн. */
    val isSimple: Boolean
        get() = tests.size == 1 && actions.all {
            it.optString("type") in setOf("fileinto", "addflag", "addsystemflag")
        }

    fun withEnable(value: Boolean): MailFilterRule =
        MailFilterRule(JSONObject(raw.toString()).put("enable", value))

    companion object {
        fun simple(
            name: String,
            field: String,
            contains: String,
            action: String,
            targetFolder: String,
            priority: Int = 10,
        ): MailFilterRule {
            val actions = JSONArray()
            when (action) {
                "markread" -> actions.put(
                    JSONObject().put("type", "addsystemflag").put("flag", "\\Seen"),
                )
                else -> actions.put(
                    JSONObject().put("type", "fileinto").put("mailbox", targetFolder),
                )
            }
            return MailFilterRule(
                JSONObject()
                    .put("name", name)
                    .put("enable", true)
                    .put("operator", "allof")
                    .put("priority", priority)
                    .put(
                        "tests",
                        JSONArray().put(
                            JSONObject()
                                .put("field", field)
                                .put("operator", "contains")
                                .put("values", JSONArray().put(contains)),
                        ),
                    )
                    .put("actions", actions),
            )
        }
    }
}
