package com.pixelized.desktop.lwa.repository.network

import com.pixelized.desktop.lwa.utils.extention.connectWebSocket
import com.pixelized.shared.lwa.protocol.websocket.SocketMessage
import io.ktor.client.HttpClient
import io.ktor.client.engine.okhttp.OkHttp
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.websocket.WebSockets
import io.ktor.serialization.kotlinx.json.json
import io.ktor.websocket.Frame
import io.ktor.websocket.readText
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json

class NetworkRepository(
    private val json: Json,
) {
    private val scope: CoroutineScope = CoroutineScope(Dispatchers.Default)
    private var networkJob: Job? = null
    private var client: HttpClient? = null

    private val outgoingMessageBuffer = MutableSharedFlow<SocketMessage>()
    private val incomingMessageBuffer = MutableSharedFlow<SocketMessage>()
    val data: SharedFlow<SocketMessage> get() = incomingMessageBuffer

    private val _status = MutableStateFlow(Status.DISCONNECTED)
    val status: StateFlow<Status> get() = _status

    fun connect(
        host: String,
        port: Int,
        onConnect: () -> Unit = { },
        onFailure: (Exception) -> Unit = { },
        onClose: () -> Unit = { },
    ) {
        client = buildNewClient()

        networkJob?.cancel()
        networkJob = scope.launch(Dispatchers.IO) {
            try {
                client?.connectWebSocket(host = host, port = port) {
                    _status.value = Status.CONNECTED
                    onConnect()

                    val job = launch {
                        // send message to the server
                        outgoingMessageBuffer.collect { message ->
                            val data = json.encodeToString(message)
                            val frame = Frame.Text(text = data)
                            send(frame = frame)
                        }
                    }
                    runCatching {
                        // watching for server incoming message
                        incoming.consumeEach { frame ->
                            if (frame is Frame.Text) {
                                val data = frame.readText()
                                val message = json.decodeFromString<SocketMessage>(data)
                                incomingMessageBuffer.emit(message)
                            }
                        }
                    }.onFailure { exception ->
                        // TODO proper exception handling
                        println("WebSocket exception: ${exception.localizedMessage}")
                    }.also {
                        job.cancel()
                    }
                }
            } catch (exception: Exception) {
                onFailure(exception)
            } finally {
                _status.value = Status.DISCONNECTED
                onClose()
            }
        }
    }

    fun disconnect() {
        networkJob?.cancel()
        scope.launch {
            client?.close()
            client = null
        }
    }

    suspend fun share(
        message: SocketMessage,
    ) {
        if (status.value == Status.CONNECTED) {
            // emit the message into the outgoing buffer
            outgoingMessageBuffer.emit(message)
        }
    }

    private fun buildNewClient() = HttpClient(
        engine = OkHttp.create(),
    ) {
        install(WebSockets) {
            pingIntervalMillis = 20_000
        }
        install(ContentNegotiation) {
            json(json)
        }
    }

    enum class Status {
        CONNECTED,
        DISCONNECTED
    }
}