package net.buzzert.kordophone.backend.server

import android.util.Log
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import net.buzzert.kordophone.backend.model.Conversation
import net.buzzert.kordophone.backend.model.Message
import net.buzzert.kordophone.backend.model.UpdateItem
import okhttp3.Response
import okhttp3.WebSocket
import okhttp3.WebSocketListener
import okio.ByteString
import java.lang.reflect.Type

const val UPMON_LOG: String = "UpdateMonitor"

class UpdateMonitor(private val client: APIClient) : WebSocketListener() {
    // Flow for getting conversation changed notifications
    val conversationChanged: Flow<Conversation>
        get() = _conversationChanged

    // Flow for messages added notifications
    val messageAdded: Flow<Message>
        get() = _messageAdded

    private val gson: Gson = Gson()
    private val updateItemsType: Type = object : TypeToken<ArrayList<UpdateItem>>() {}.type
    private var webSocket: WebSocket? = null
    private var needsSocketReconnect: Boolean = false
    private var messageSeq: Int = -1

    private val _conversationChanged: MutableSharedFlow<Conversation> = MutableSharedFlow()
    private val _messageAdded: MutableSharedFlow<Message> = MutableSharedFlow()

    fun beginMonitoringUpdates() {
        if (!client.isConfigured) {
            Log.e(UPMON_LOG, "Closing websocket connection because client is not configured.")
            return
        }

        Log.d(UPMON_LOG, "Opening websocket connection")
        try {
            this.webSocket = client.getWebSocketClient(
                serverPath = "updates",
                queryParams = mapOf("seq" to messageSeq.toString()),
                listener = this
            )
        } catch (e: Error) {
            Log.e(UPMON_LOG, "Error getting websocket client: ${e.message}")
        }
    }

    fun stopMonitoringForUpdates() {
        this.webSocket?.close(1000, "Closing on program request.")
    }

    private fun processEncodedSocketMessage(message: String) = runBlocking {
        val reader = message.reader()
        val jsonReader = gson.newJsonReader(reader)

        val updateItems: List<UpdateItem> = gson.fromJson(message, updateItemsType)
        for (updateItem: UpdateItem in updateItems) {
            val conversationChanged = updateItem.conversationChanged
            if (conversationChanged != null) {
                _conversationChanged.emit(conversationChanged)
            }

            if (updateItem.messageAdded != null) {
                _messageAdded.emit(updateItem.messageAdded.also {
                    it.conversation = conversationChanged!!
                })
            }

            if (updateItem.sequence > messageSeq) {
                messageSeq = updateItem.sequence
            }
        }
    }

    @OptIn(DelicateCoroutinesApi::class)
    private fun setNeedsSocketReconnect() {
        if (!needsSocketReconnect) {
            needsSocketReconnect = true

            GlobalScope.launch {
                needsSocketReconnect = false

                // Delay 5 seconds
                delay(5000L)

                beginMonitoringUpdates()
            }
        }
    }

    // <WebSocketListener>

    override fun onOpen(webSocket: WebSocket, response: Response) {
        super.onOpen(webSocket, response)
        Log.d(UPMON_LOG, "Update monitor websocket open")
    }

    override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
        super.onClosed(webSocket, code, reason)
        Log.d(UPMON_LOG, "Update monitor socket closed")
        setNeedsSocketReconnect()
    }

    override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
        super.onFailure(webSocket, t, response)
        Log.d(UPMON_LOG, "Update monitor socket failure: ${t.message} :: Response: ${response?.body()}. Reconnecting in 5 seconds.")
        setNeedsSocketReconnect()
    }

    override fun onMessage(webSocket: WebSocket, text: String) {
        super.onMessage(webSocket, text)
        Log.d(UPMON_LOG, "Update monitor websocket received text message")
        processEncodedSocketMessage(text)
    }

    override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
        super.onMessage(webSocket, bytes)
        Log.d(UPMON_LOG, "Update monitor websocket received bytes message")
        processEncodedSocketMessage(bytes.utf8())
    }
}