package ru.forbion.f7cloud.feature.talk

import okhttp3.FormBody
import okhttp3.MediaType.Companion.toMediaType
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 TalkRepository {
    suspend fun listRooms(session: AuthSession): List<TalkRoom> {
        val client = authedClient(session)
        val url = spreedApi(session, "v4") + "/room?includeStatus=true&format=json"
        val data = getOcsArray(client, url)
        val out = mutableListOf<TalkRoom>()
        for (i in 0 until data.length()) {
            parseRoom(data.optJSONObject(i))?.let { out += it }
        }
        return out.sortedByDescending { it.lastActivity }
    }

    suspend fun loadMessages(
        session: AuthSession,
        roomToken: String,
        lastKnownMessageId: Long = 0L,
        lookIntoFuture: Boolean = false,
        setReadMarker: Boolean = false,
        timeoutSeconds: Int = 30,
    ): List<TalkMessage> {
        val client = authedClient(session)
        val look = if (lookIntoFuture) 1 else 0
        val readMarker = if (setReadMarker) 1 else 0
        val url = spreedApi(session, "v1") + "/chat/$roomToken" +
            "?lookIntoFuture=$look&limit=50&lastKnownMessageId=$lastKnownMessageId" +
            "&setReadMarker=$readMarker&timeout=$timeoutSeconds&format=json"
        val data = getOcsArray(client, url)
        return parseMessages(data)
    }

    suspend fun setReadMarker(session: AuthSession, roomToken: String, lastReadMessage: Long? = null) {
        val client = authedClient(session)
        val url = spreedApi(session, "v1") + "/chat/$roomToken/read?format=json"
        val payload = if (lastReadMessage != null) {
            JSONObject().put("lastReadMessage", lastReadMessage).toString()
        } else {
            "{}"
        }
        val request = Request.Builder()
            .url(url)
            .header("OCS-APIRequest", "true")
            .header("Accept", "application/json")
            .post(payload.toRequestBody("application/json; charset=utf-8".toMediaType()))
            .build()
        client.newCall(request).execute().use { response ->
            if (!response.isSuccessful) {
                error("Talk read marker failed: HTTP ${response.code}")
            }
        }
    }

    suspend fun listParticipants(session: AuthSession, roomToken: String): List<TalkParticipant> {
        val client = authedClient(session)
        val url = spreedApi(session, "v4") + "/room/$roomToken/participants?includeStatus=true&format=json"
        val data = getOcsArray(client, url)
        val out = mutableListOf<TalkParticipant>()
        for (i in 0 until data.length()) {
            parseParticipant(data.optJSONObject(i))?.let { out += it }
        }
        return out.sortedWith(compareBy({ it.participantType }, { it.displayName.lowercase() }))
    }

    suspend fun searchMentions(session: AuthSession, roomToken: String, query: String): List<TalkMention> {
        val client = authedClient(session)
        val encoded = java.net.URLEncoder.encode(query, "UTF-8")
        val url = spreedApi(session, "v1") + "/chat/$roomToken/mentions" +
            "?search=$encoded&limit=8&includeStatus=true&format=json"
        val data = getOcsArray(client, url)
        val out = mutableListOf<TalkMention>()
        for (i in 0 until data.length()) {
            val item = data.optJSONObject(i) ?: continue
            val mentionId = item.optString("mentionId").ifBlank { item.optString("id") }
            if (mentionId.isBlank()) continue
            out += TalkMention(
                mentionId = mentionId,
                id = item.optString("id"),
                label = item.optString("label").ifBlank { mentionId },
                source = item.optString("source", "users"),
            )
        }
        return out
    }

    suspend fun searchUsers(session: AuthSession, query: String): List<TalkUserCandidate> {
        val trimmed = query.trim()
        if (trimmed.length < 2) return emptyList()
        val client = authedClient(session)
        val encoded = java.net.URLEncoder.encode(trimmed, "UTF-8")
        val url = "${session.serverUrl.trimEnd('/')}/ocs/v2.php/core/autocomplete/get" +
            "?search=$encoded&itemType=&itemId=&shareTypes[]=0&shareTypes[]=1&shareTypes[]=2&format=json"
        val data = getOcsArray(client, url)
        val out = mutableListOf<TalkUserCandidate>()
        for (i in 0 until data.length()) {
            val item = data.optJSONObject(i) ?: continue
            if (item.optString("source") != "users") continue
            val userId = item.optString("id")
            if (userId.isBlank() || userId == session.username) continue
            out += TalkUserCandidate(
                userId = userId,
                displayName = item.optString("label").ifBlank { userId },
                source = item.optString("source", "users"),
            )
        }
        return out.distinctBy { it.userId }
    }

    suspend fun createOneToOneRoom(session: AuthSession, userId: String): TalkRoom {
        val client = authedClient(session)
        val url = spreedApi(session, "v4") + "/room?format=json"
        val payload = JSONObject()
            .put("roomType", 1)
            .put("invite", userId)
            .put("source", "users")
            .toString()
        val room = postOcsObject(client, url, payload)
        return parseRoom(room) ?: error("Не удалось создать личный чат")
    }

    suspend fun createGroupRoom(session: AuthSession, name: String, userIds: List<String>): TalkRoom {
        val client = authedClient(session)
        val url = spreedApi(session, "v4") + "/room?format=json"
        val participants = JSONObject()
        if (userIds.isNotEmpty()) {
            participants.put("users", JSONArray(userIds))
        }
        val payload = JSONObject()
            .put("roomType", 2)
            .put("roomName", name.trim())
            .put("readOnly", 0)
            .put("listable", 0)
            .put("lobbyState", 0)
            .put("sipEnabled", 0)
            .put("permissions", 0)
            .put("recordingConsent", 0)
            .put("mentionPermissions", 0)
            .put("participants", participants)
            .toString()
        val room = postOcsObject(client, url, payload)
        return parseRoom(room) ?: error("Не удалось создать групповой чат")
    }

    suspend fun setFavorite(session: AuthSession, roomToken: String, favorite: Boolean) {
        val client = authedClient(session)
        val url = spreedApi(session, "v4") + "/room/$roomToken/favorite?format=json"
        val builder = Request.Builder()
            .url(url)
            .header("OCS-APIRequest", "true")
            .header("Accept", "application/json")
        if (favorite) {
            builder.post("{}".toRequestBody("application/json; charset=utf-8".toMediaType()))
        } else {
            builder.delete()
        }
        client.newCall(builder.build()).execute().use { response ->
            if (!response.isSuccessful) error("Talk favorite failed: HTTP ${response.code}")
        }
    }

    suspend fun markUnread(session: AuthSession, roomToken: String) {
        val client = authedClient(session)
        val url = spreedApi(session, "v1") + "/chat/$roomToken/read?format=json"
        val request = Request.Builder()
            .url(url)
            .header("OCS-APIRequest", "true")
            .header("Accept", "application/json")
            .delete()
            .build()
        client.newCall(request).execute().use { response ->
            if (!response.isSuccessful) error("Talk mark unread failed: HTTP ${response.code}")
        }
    }

    suspend fun addParticipant(session: AuthSession, roomToken: String, userId: String) {
        val client = authedClient(session)
        val url = spreedApi(session, "v4") + "/room/$roomToken/participants?format=json"
        val payload = JSONObject()
            .put("newParticipant", userId)
            .put("source", "users")
            .toString()
        postOcsObject(client, url, payload)
    }

    suspend fun sendMessage(
        session: AuthSession,
        roomToken: String,
        message: String,
        replyTo: Long? = null,
    ) {
        val client = authedClient(session)
        val url = spreedApi(session, "v1") + "/chat/$roomToken?format=json"
        val payload = JSONObject().put("message", message)
        if (replyTo != null && replyTo > 0L) {
            payload.put("replyTo", replyTo)
        }
        val request = Request.Builder()
            .url(url)
            .header("OCS-APIRequest", "true")
            .header("Accept", "application/json")
            .post(payload.toString().toRequestBody("application/json; charset=utf-8".toMediaType()))
            .build()
        client.newCall(request).execute().use { response ->
            if (response.code != 201 && !response.isSuccessful) {
                error("Talk send failed: HTTP ${response.code}")
            }
        }
    }

    suspend fun addReaction(session: AuthSession, roomToken: String, messageId: Long, emoji: String) {
        val client = authedClient(session)
        val encoded = java.net.URLEncoder.encode(emoji, "UTF-8")
        val url = spreedApi(session, "v1") + "/reaction/$roomToken/$messageId?reaction=$encoded&format=json"
        postEmpty(client, url, "POST")
    }

    suspend fun removeReaction(session: AuthSession, roomToken: String, messageId: Long, emoji: String) {
        val client = authedClient(session)
        val encoded = java.net.URLEncoder.encode(emoji, "UTF-8")
        val url = spreedApi(session, "v1") + "/reaction/$roomToken/$messageId?reaction=$encoded&format=json"
        postEmpty(client, url, "DELETE")
    }

    suspend fun searchMessages(session: AuthSession, roomToken: String, query: String): List<TalkMessage> {
        val trimmed = query.trim()
        if (trimmed.length < 2) return emptyList()
        val client = authedClient(session)
        val encoded = java.net.URLEncoder.encode(trimmed, "UTF-8")
        val url = spreedApi(session, "v1") + "/chat/$roomToken?search=$encoded&limit=30&format=json"
        val data = getOcsArray(client, url)
        return parseMessages(data)
    }

    suspend fun getRoom(session: AuthSession, roomToken: String): TalkRoom? {
        val client = authedClient(session)
        val url = spreedApi(session, "v4") + "/room/$roomToken?format=json"
        val ocs = getOcsResponse(client, url)
        return parseRoom(ocs.optJSONObject("data"))
    }

    suspend fun setNotificationLevel(session: AuthSession, roomToken: String, level: Int) {
        val client = authedClient(session)
        val url = spreedApi(session, "v4") + "/room/$roomToken/notify?format=json"
        val payload = JSONObject().put("level", level).toString()
        postOcsObject(client, url, payload)
    }

    private fun postEmpty(client: OkHttpClient, url: String, method: String) {
        val builder = Request.Builder()
            .url(url)
            .header("OCS-APIRequest", "true")
            .header("Accept", "application/json")
        when (method) {
            "POST" -> builder.post("{}".toRequestBody("application/json; charset=utf-8".toMediaType()))
            "DELETE" -> builder.delete()
        }
        client.newCall(builder.build()).execute().use { response ->
            if (!response.isSuccessful) error("Talk request failed: HTTP ${response.code}")
        }
    }

    suspend fun editMessage(session: AuthSession, roomToken: String, messageId: Long, newText: String) {
        val trimmed = newText.trim()
        if (trimmed.isEmpty()) error("Сообщение пустое")
        val client = authedClient(session)
        val url = spreedApi(session, "v1") + "/chat/$roomToken/$messageId?format=json"
        val body = FormBody.Builder().add("message", trimmed).build()
        val request = Request.Builder()
            .url(url)
            .header("OCS-APIRequest", "true")
            .header("Accept", "application/json")
            .put(body)
            .build()
        client.newCall(request).execute().use { response ->
            if (!response.isSuccessful) error("Talk edit failed: HTTP ${response.code}")
        }
    }

    suspend fun uploadAttachment(
        session: AuthSession,
        roomToken: String,
        fileName: String,
        mimeType: String,
        bytes: ByteArray,
        caption: String = "",
        replyTo: Long? = null,
    ) {
        TalkAttachmentUploader.uploadAndShare(
            session = session,
            roomToken = roomToken,
            fileName = fileName,
            mimeType = mimeType,
            bytes = bytes,
            caption = caption,
            replyTo = replyTo,
        )
    }

    suspend fun deleteMessage(session: AuthSession, roomToken: String, messageId: Long) {
        val client = authedClient(session)
        val url = spreedApi(session, "v1") + "/chat/$roomToken/$messageId?format=json"
        val request = Request.Builder()
            .url(url)
            .header("OCS-APIRequest", "true")
            .header("Accept", "application/json")
            .delete()
            .build()
        client.newCall(request).execute().use { response ->
            if (!response.isSuccessful) error("Talk delete failed: HTTP ${response.code}")
        }
    }

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

    private fun spreedApi(session: AuthSession, version: String): String =
        "${session.serverUrl.trimEnd('/')}/ocs/v2.php/apps/spreed/api/$version"

    private fun getOcsArray(client: OkHttpClient, url: String): JSONArray {
        val ocs = getOcsResponse(client, url)
        return when (val data = ocs.opt("data")) {
            is JSONArray -> data
            is JSONObject -> JSONArray().put(data)
            else -> JSONArray()
        }
    }

    private fun postOcsObject(client: OkHttpClient, url: String, payload: String): JSONObject {
        val request = Request.Builder()
            .url(url)
            .header("OCS-APIRequest", "true")
            .header("Accept", "application/json")
            .post(payload.toRequestBody("application/json; charset=utf-8".toMediaType()))
            .build()
        client.newCall(request).execute().use { response ->
            if (!response.isSuccessful || response.body == null) {
                error("Talk request failed: HTTP ${response.code}")
            }
            val ocs = JSONObject(response.body!!.string()).optJSONObject("ocs")
                ?: error("Talk request failed: invalid response")
            if (ocs.optJSONObject("meta")?.optString("status") == "failure") {
                error(ocs.optJSONObject("meta")?.optString("message") ?: "Talk request failed")
            }
            return ocs.optJSONObject("data") ?: JSONObject()
        }
    }

    private fun getOcsResponse(client: OkHttpClient, url: String): JSONObject {
        val request = Request.Builder()
            .url(url)
            .header("OCS-APIRequest", "true")
            .header("Accept", "application/json")
            .build()
        client.newCall(request).execute().use { response ->
            if (!response.isSuccessful || response.body == null) {
                error("Talk request failed: HTTP ${response.code}")
            }
            val ocs = JSONObject(response.body!!.string()).optJSONObject("ocs")
                ?: error("Talk request failed: invalid response")
            if (ocs.optJSONObject("meta")?.optString("status") == "failure") {
                error(ocs.optJSONObject("meta")?.optString("message") ?: "Talk request failed")
            }
            return ocs
        }
    }

    private fun parseParticipant(item: JSONObject?): TalkParticipant? {
        if (item == null) return null
        val actorId = item.optString("actorId")
        if (actorId.isBlank()) return null
        return TalkParticipant(
            actorId = actorId,
            actorType = item.optString("actorType"),
            displayName = item.optString("displayName").ifBlank { actorId },
            participantType = item.optInt("participantType", 3),
            inCall = item.optInt("inCall", 0),
            status = item.optString("status"),
            statusMessage = item.optString("statusMessage").ifBlank { null },
        )
    }

    private fun parseRoom(room: JSONObject?): TalkRoom? {
        if (room == null) return null
        val token = room.optString("token")
        if (token.isBlank()) return null
        val name = room.optString("displayName").ifBlank { room.optString("name") }
        val lastMessage = room.optJSONObject("lastMessage")
        val lastParams = parseMessageParameters(lastMessage?.optJSONObject("messageParameters"))
        val preview = previewText(lastMessage?.optString("message").orEmpty(), lastParams)
        return TalkRoom(
            token = token,
            displayName = name.ifBlank { token },
            type = room.optInt("type", 0),
            unreadMessages = room.optInt("unreadMessages", 0),
            unreadMention = room.optBoolean("unreadMention", false),
            hasCall = room.optBoolean("hasCall", false),
            isFavorite = room.optBoolean("isFavorite", false),
            lastActivity = room.optLong("lastActivity", 0L),
            lastMessagePreview = preview,
            lastMessageTimestamp = lastMessage?.optLong("timestamp", 0L) ?: 0L,
            actorId = room.optString("actorId"),
            actorType = room.optString("actorType"),
            lastReadMessage = room.optLong("lastReadMessage", 0L),
        )
    }

    private fun parseMessages(data: JSONArray): List<TalkMessage> {
        val out = mutableListOf<TalkMessage>()
        for (i in 0 until data.length()) {
            val item = data.optJSONObject(i) ?: continue
            val id = item.optLong("id", 0L)
            val actor = item.optString("actorDisplayName").ifBlank { item.optString("actorId") }
            val text = item.optString("message")
            val timestamp = item.optLong("timestamp", 0L)
            val messageType = item.optString("messageType", "comment")
            val replyTo = item.optLong("replyTo", 0L).takeIf { it > 0L }
            val reactions = parseReactions(item.optJSONObject("reactions"))
            val messageParameters = parseMessageParameters(item.optJSONObject("messageParameters"))
            val attachment = parseAttachment(item.optJSONObject("messageParameters"))
            if (id > 0L || text.isNotBlank()) {
                out += TalkMessage(
                    id = id,
                    actorDisplayName = actor,
                    actorId = item.optString("actorId"),
                    text = text,
                    timestamp = timestamp,
                    messageType = messageType,
                    status = TalkMessage.Status.SENT,
                    replyToId = replyTo,
                    reactions = reactions,
                    messageParameters = messageParameters,
                    attachmentFileId = attachment?.fileId,
                    attachmentFileName = attachment?.fileName,
                    attachmentMimeType = attachment?.mimeType,
                    attachmentLink = attachment?.link,
                )
            }
        }
        return out.sortedBy { it.id }
    }

    private data class ParsedAttachment(
        val fileId: String?,
        val fileName: String?,
        val mimeType: String?,
        val link: String?,
    )

    private fun parseMessageParameters(params: JSONObject?): Map<String, TalkMessageParameter> {
        if (params == null) return emptyMap()
        return buildMap {
            params.keys().forEach { key ->
                val obj = params.optJSONObject(key) ?: return@forEach
                val type = obj.optString("type")
                val name = obj.optString("name")
                if (name.isBlank() && type != "geo-location") return@forEach
                put(
                    key,
                    TalkMessageParameter(
                        type = type,
                        name = name,
                        id = obj.optString("id"),
                    ),
                )
            }
        }
    }

    private fun parseAttachment(params: JSONObject?): ParsedAttachment? {
        if (params == null) return null
        val file = params.optJSONObject("file") ?: params.optJSONObject("object") ?: return null
        return ParsedAttachment(
            fileId = file.optString("id").ifBlank { null },
            fileName = file.optString("name").ifBlank { file.optString("filename").ifBlank { null } },
            mimeType = file.optString("mimetype").ifBlank { file.optString("mimeType").ifBlank { null } },
            link = file.optString("link").ifBlank { null },
        )
    }

    private fun parseReactions(obj: JSONObject?): Map<String, Int> {
        if (obj == null) return emptyMap()
        return buildMap {
            obj.keys().forEach { key ->
                put(key, obj.optInt(key, 0))
            }
        }
    }

    private fun previewText(raw: String, parameters: Map<String, TalkMessageParameter> = emptyMap()): String =
        formatMessageText(formatSystemMessage(raw, parameters)).take(120)
}
