package ru.forbion.f7cloud.feature.f7support

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
import ru.forbion.f7cloud.core.network.applyOcsJson
import ru.forbion.f7cloud.core.network.ocsData
import ru.forbion.f7cloud.core.network.ocsMeta
import ru.forbion.f7cloud.core.network.parseJsonArray
import ru.forbion.f7cloud.core.network.parseJsonObject
import java.net.URI

class SupportRepository {
    suspend fun loadConfig(session: AuthSession): SupportConfig {
        val client = NetworkFactory.newAuthedClient(session.username, session.appPassword, session.trustAllCerts)
        val request = Request.Builder()
            .url("${session.serverUrl.trimEnd('/')}/ocs/v2.php/apps/f7support/api/v1/mobile-config?format=json")
            .applyOcsJson()
            .build()
        client.newCall(request).execute().use { response ->
            if (!response.isSuccessful || response.body == null) {
                error("Support config HTTP ${response.code}")
            }
            val data = parseJsonObject(response.body!!.string(), "конфигурация поддержки").ocsData()
            val apiBase = data?.optString("supportApiBase").orEmpty().ifBlank {
                "https://support.f7cloud.ru"
            }
            val serverAddress = data?.optString("serverAddress").orEmpty().ifBlank {
                hostFromUrl(session.serverUrl)
            }
            return SupportConfig(
                supportApiBase = apiBase.trimEnd('/'),
                serverAddress = serverAddress,
                clientReadReceipts = data?.optBoolean("clientReadReceipts") == true,
                isSupportAdmin = data?.optBoolean("isSupportAdmin") == true,
                username = session.username,
                serverUrl = session.serverUrl.trimEnd('/'),
                appPassword = session.appPassword,
                trustAllCerts = session.trustAllCerts,
            )
        }
    }

    suspend fun listTickets(config: SupportConfig): List<SupportTicket> {
        val client = supportClient()
        val request = Request.Builder()
            .url("${config.supportApiBase}/api/client/tickets")
            .headers(identityHeaders(config))
            .build()
        client.newCall(request).execute().use { response ->
            if (!response.isSuccessful || response.body == null) {
                error("Не удалось загрузить обращения (${response.code})")
            }
            val array = parseJsonArray(response.body!!.string(), "список обращений")
            val out = mutableListOf<SupportTicket>()
            for (i in 0 until array.length()) {
                val obj = array.optJSONObject(i) ?: continue
                parseTicket(obj)?.let { out += it }
            }
            return out
        }
    }

    suspend fun loadMessages(config: SupportConfig, ticketNumber: String): List<SupportMessage> {
        val client = supportClient()
        val request = Request.Builder()
            .url("${config.supportApiBase}/api/client/tickets/${encodePath(ticketNumber)}/messages")
            .headers(identityHeaders(config))
            .build()
        client.newCall(request).execute().use { response ->
            if (!response.isSuccessful || response.body == null) {
                error("Не удалось загрузить сообщения (${response.code})")
            }
            val array = parseJsonArray(response.body!!.string(), "сообщения обращения")
            val out = mutableListOf<SupportMessage>()
            for (i in 0 until array.length()) {
                val obj = array.optJSONObject(i) ?: continue
                val id = obj.optLong("id", obj.optLong("message_id", 0L))
                val role = obj.optString("author_role")
                val author = obj.optString("author")
                val outgoing = messageIsOutgoing(config, role, author)
                out += SupportMessage(
                    id = id,
                    text = obj.optString("text"),
                    createdAt = obj.optString("created_at"),
                    outgoing = outgoing,
                    authorLabel = supportSenderLabel(config, role, author, outgoing),
                    attachments = parseAttachments(obj.optJSONArray("attachments")),
                    read = obj.optBoolean("read_by_operator", false) ||
                        obj.optBoolean("is_read", false),
                )
            }
            return out
        }
    }

    suspend fun sendMessage(config: SupportConfig, ticketNumber: String, text: String): Long {
        val client = supportClient()
        val payload = JSONObject().put("text", text).toString()
        val request = Request.Builder()
            .url("${config.supportApiBase}/api/client/tickets/${encodePath(ticketNumber)}/messages")
            .headers(identityHeaders(config))
            .post(payload.toRequestBody("application/json; charset=utf-8".toMediaType()))
            .build()
        client.newCall(request).execute().use { response ->
            if (!response.isSuccessful || response.body == null) {
                error("Не удалось отправить сообщение (${response.code})")
            }
            val json = parseJsonObject(response.body!!.string(), "ответ на сообщение")
            return json.optLong("id", json.optLong("message_id", 0L))
        }
    }

    suspend fun uploadAttachment(
        config: SupportConfig,
        ticketNumber: String,
        messageId: Long,
        file: SupportPendingFile,
    ) {
        val client = supportClient()
        val mediaType = file.mimeType.toMediaTypeOrNull() ?: "application/octet-stream".toMediaType()
        val body = MultipartBody.Builder()
            .setType(MultipartBody.FORM)
            .addFormDataPart("message_id", messageId.toString())
            .addFormDataPart(
                "file",
                file.fileName,
                file.bytes.toRequestBody(mediaType),
            )
            .build()
        val request = Request.Builder()
            .url("${config.supportApiBase}/api/client/tickets/${encodePath(ticketNumber)}/attachments")
            .headers(identityHeaders(config))
            .post(body)
            .build()
        client.newCall(request).execute().use { response ->
            if (!response.isSuccessful) {
                error("Вложение не принято (${response.code})")
            }
        }
    }

    suspend fun sendMessageWithAttachments(
        config: SupportConfig,
        ticketNumber: String,
        text: String,
        attachments: List<SupportPendingFile>,
    ) {
        val trimmed = text.trim()
        val messageText = when {
            trimmed.isNotEmpty() -> trimmed
            attachments.isNotEmpty() -> MESSAGE_BODY_PLACEHOLDER
            else -> error("Введите сообщение или прикрепите файл")
        }
        val messageId = sendMessage(config, ticketNumber, messageText)
        if (attachments.isEmpty()) return
        if (messageId <= 0L) {
            error("Сообщение создано, но сервер не вернул id — вложения не отправлены")
        }
        attachments.forEach { file ->
            uploadAttachment(config, ticketNumber, messageId, file)
        }
    }

    suspend fun downloadAttachment(
        config: SupportConfig,
        ticketNumber: String,
        attachmentId: Long,
    ): Pair<ByteArray, String> {
        val client = supportClient()
        val request = Request.Builder()
            .url(
                "${config.supportApiBase}/api/client/tickets/${encodePath(ticketNumber)}" +
                    "/attachments/${encodePath(attachmentId.toString())}",
            )
            .headers(identityHeaders(config))
            .build()
        client.newCall(request).execute().use { response ->
            if (!response.isSuccessful || response.body == null) {
                error("Не удалось скачать вложение (${response.code})")
            }
            val mime = response.header("Content-Type")?.substringBefore(';')?.trim()
                ?: "application/octet-stream"
            return response.body!!.bytes() to mime
        }
    }

    suspend fun createTicket(
        config: SupportConfig,
        subject: String,
        body: String,
        attachments: List<SupportPendingFile> = emptyList(),
    ): String {
        val client = supportClient()
        val payload = JSONObject()
            .put("server_address", config.serverAddress)
            .put("username", config.username)
            .put("subject", subject)
            .put("body", TICKET_CREATE_BODY_PLACEHOLDER)
            .put("duplicate", 0)
            .toString()
        val createReq = Request.Builder()
            .url("${config.supportApiBase}/api/client/tickets")
            .headers(identityHeaders(config))
            .post(payload.toRequestBody("application/json; charset=utf-8".toMediaType()))
            .build()
        val ticketNumber = client.newCall(createReq).execute().use { response ->
            if (!response.isSuccessful || response.body == null) {
                error("Не удалось создать обращение (${response.code})")
            }
            parseJsonObject(response.body!!.string(), "создание обращения").optString("ticket_number")
        }
        if (ticketNumber.isBlank()) {
            error("Сервер не вернул номер обращения")
        }
        val messageId = sendMessage(config, ticketNumber, body.trim())
        if (attachments.isNotEmpty()) {
            if (messageId <= 0L) {
                error("Обращение создано, но вложения не отправлены — откройте чат и прикрепите файлы вручную")
            } else {
                attachments.forEach { file ->
                    uploadAttachment(config, ticketNumber, messageId, file)
                }
            }
        }
        return ticketNumber
    }

    suspend fun markRead(config: SupportConfig, ticketNumber: String) {
        if (!config.clientReadReceipts) return
        val client = supportClient()
        val request = Request.Builder()
            .url("${config.supportApiBase}/api/client/tickets/${encodePath(ticketNumber)}/read")
            .headers(identityHeaders(config))
            .post("".toRequestBody(null))
            .build()
        client.newCall(request).execute().use { response ->
            if (response.code == 404) return
        }
    }

    suspend fun submitComplaint(
        config: SupportConfig,
        ticketNumber: String,
        ticketSubject: String,
        text: String,
    ) {
        val client = NetworkFactory.newAuthedClient(config.username, config.appPassword, config.trustAllCerts)
        val payload = JSONObject()
            .put("ticketNumber", ticketNumber)
            .put("ticketSubject", ticketSubject)
            .put("text", text)
            .toString()
        val request = Request.Builder()
            .url("${config.serverUrl}/ocs/v2.php/apps/f7support/api/v1/complaint?format=json")
            .applyOcsJson()
            .post(payload.toRequestBody("application/json; charset=utf-8".toMediaType()))
            .build()
        client.newCall(request).execute().use { response ->
            if (!response.isSuccessful) {
                val body = response.body?.string().orEmpty()
                val err = runCatching {
                    parseJsonObject(body, "жалоба").ocsMeta()?.optString("message")
                }.getOrNull().orEmpty()
                error(err.ifBlank { "Не удалось отправить жалобу (${response.code})" })
            }
        }
    }

    private fun parseTicket(obj: JSONObject): SupportTicket? {
        val number = obj.optString("ticket_number")
        if (number.isBlank()) return null
        val bitrixRaw = obj.opt("bitrix_deal_id")
        val bitrixDealId = when (bitrixRaw) {
            is Number -> bitrixRaw.toLong().takeIf { it > 0 }
            else -> bitrixRaw?.toString()?.trim()?.toLongOrNull()?.takeIf { it > 0 }
        }
        return SupportTicket(
            ticketNumber = number,
            subject = obj.optString("subject").ifBlank { "—" },
            status = obj.optString("status"),
            preview = obj.optString("preview_text"),
            hasUnread = obj.optBoolean("has_unread", false),
            activityAt = obj.optString("activity_at").ifBlank { obj.optString("created_at") },
            createdAt = obj.optString("created_at"),
            bitrixDealId = bitrixDealId,
            assignedEmployee = obj.optString("assigned_employee"),
            clientUsername = obj.optString("client_username"),
        )
    }

    private fun parseAttachments(array: JSONArray?): List<SupportAttachment> {
        if (array == null || array.length() == 0) return emptyList()
        val out = mutableListOf<SupportAttachment>()
        for (i in 0 until array.length()) {
            val obj = array.optJSONObject(i) ?: continue
            val id = obj.optLong("id", obj.optLong("attachment_id", 0L))
            if (id <= 0L) continue
            out += SupportAttachment(
                id = id,
                filename = obj.optString("filename").ifBlank { "file" },
                mimeType = obj.optString("mime_type"),
                sizeBytes = obj.optLong("size_bytes", -1L).takeIf { it >= 0 },
            )
        }
        return out
    }

    private fun messageIsOutgoing(config: SupportConfig, role: String, author: String): Boolean {
        val r = role.lowercase()
        val a = author.lowercase()
        val u = config.username.lowercase()
        if (config.isSupportAdmin && r == "support" && u.isNotEmpty() && a == u) return true
        if (r == "client" || r == "user") return true
        return u.isNotEmpty() && a == u
    }

    private fun supportSenderLabel(
        config: SupportConfig,
        role: String,
        author: String,
        outgoing: Boolean,
    ): String {
        if (outgoing) return "Вы"
        val name = author.trim()
        if (name.isNotEmpty()) return name
        return if (role.equals("support", true)) "Поддержка" else "Клиент"
    }

    private fun supportClient(): OkHttpClient = OkHttpClient.Builder().build()

    private fun identityHeaders(config: SupportConfig): okhttp3.Headers {
        val builder = okhttp3.Headers.Builder()
            .add("X-F7cloud-User", config.username)
            .add("X-F7cloud-Server", config.serverAddress)
        if (config.isSupportAdmin) {
            builder.add("X-F7cloud-Support-Admin", "1")
        }
        return builder.build()
    }

    private fun encodePath(segment: String): String {
        return java.net.URLEncoder.encode(segment, Charsets.UTF_8.name())
    }

    private fun hostFromUrl(serverUrl: String): String {
        return runCatching { URI(serverUrl.trimEnd('/')).host }.getOrNull().orEmpty()
    }

    companion object {
        private const val MESSAGE_BODY_PLACEHOLDER = "."
        private const val TICKET_CREATE_BODY_PLACEHOLDER = "."
    }
}

data class SupportConfig(
    val supportApiBase: String,
    val serverAddress: String,
    val clientReadReceipts: Boolean,
    val isSupportAdmin: Boolean,
    val username: String,
    val serverUrl: String,
    val appPassword: String,
    val trustAllCerts: Boolean,
)

data class SupportTicket(
    val ticketNumber: String,
    val subject: String,
    val status: String,
    val preview: String,
    val hasUnread: Boolean,
    val activityAt: String,
    val createdAt: String,
    val bitrixDealId: Long? = null,
    val assignedEmployee: String = "",
    val clientUsername: String = "",
) {
    fun displayNumber(): String = bitrixDealId?.toString() ?: "—"

    fun statusBucket(): SupportStatusBucket = when (status) {
        "Закрыт" -> SupportStatusBucket.Closed
        "В работе" -> SupportStatusBucket.Progress
        "Новый" -> SupportStatusBucket.New
        else -> SupportStatusBucket.New
    }
}

enum class SupportStatusBucket(val title: String) {
    New("Новые"),
    Progress("В работе"),
    Closed("Закрыт"),
}

data class SupportMessage(
    val id: Long,
    val text: String,
    val createdAt: String,
    val outgoing: Boolean,
    val authorLabel: String,
    val attachments: List<SupportAttachment> = emptyList(),
    val read: Boolean = false,
)

data class SupportAttachment(
    val id: Long,
    val filename: String,
    val mimeType: String,
    val sizeBytes: Long? = null,
)
