package de.krisenvorrat.app.data.sync

import io.ktor.client.HttpClient
import io.ktor.client.engine.okhttp.OkHttp
import io.ktor.client.plugins.websocket.WebSockets
import io.ktor.client.plugins.websocket.webSocket
import io.ktor.websocket.Frame
import io.ktor.websocket.readText
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
internal class WebSocketClientImpl @Inject constructor() : WebSocketClient {

    private val _events = MutableSharedFlow<WebSocketEvent>(extraBufferCapacity = 16)
    override val events: SharedFlow<WebSocketEvent> = _events.asSharedFlow()

    private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
    private var connectionJob: Job? = null

    private val wsHttpClient = HttpClient(OkHttp) {
        install(WebSockets)
    }

    override fun connect(serverUrl: String, accessToken: String) {
        connectionJob?.cancel()
        connectionJob = scope.launch {
            var backoffMs = INITIAL_BACKOFF_MS
            while (isActive) {
                try {
                    val wsUrl = serverUrl.trimEnd('/')
                        .replace("https://", "wss://")
                        .replace("http://", "ws://")
                    wsHttpClient.webSocket("$wsUrl/ws/sync?token=$accessToken") {
                        backoffMs = INITIAL_BACKOFF_MS
                        _events.emit(WebSocketEvent.Connected)
                        for (frame in incoming) {
                            if (frame is Frame.Text) {
                                handleFrame(frame.readText())
                            }
                        }
                    }
                } catch (e: CancellationException) {
                    break
                } catch (_: Exception) {
                    // connection failed – retry after backoff
                }
                if (!isActive) break
                _events.emit(WebSocketEvent.Disconnected)
                delay(backoffMs)
                backoffMs = minOf(backoffMs * 2, MAX_BACKOFF_MS)
            }
        }
    }

    override fun disconnect() {
        connectionJob?.cancel()
        connectionJob = null
        scope.launch { _events.emit(WebSocketEvent.Disconnected) }
    }

    private suspend fun handleFrame(text: String) {
        try {
            val event = json.decodeFromString<WsServerEvent>(text)
            when (event.type) {
                "inventoryUpdated" -> _events.emit(WebSocketEvent.InventoryUpdated(event.itemId ?: ""))
                "fullSyncRequired" -> _events.emit(WebSocketEvent.FullSyncRequired)
                "new_message" -> {
                    val msg = de.krisenvorrat.shared.model.MessageDto(
                        id = event.id ?: return,
                        senderId = event.senderId ?: return,
                        senderUsername = event.senderUsername ?: return,
                        receiverId = event.receiverId ?: return,
                        body = event.body ?: return,
                        sentAt = event.sentAt ?: return,
                        deliveredAt = null
                    )
                    _events.emit(WebSocketEvent.NewMessage(msg))
                }
            }
        } catch (_: Exception) {
            // ignore malformed events
        }
    }

    private companion object {
        const val INITIAL_BACKOFF_MS = 2_000L
        const val MAX_BACKOFF_MS = 60_000L
        val json = Json { ignoreUnknownKeys = true }
    }
}

@Serializable
private data class WsServerEvent(
    val type: String,
    val itemId: String? = null,
    val id: String? = null,
    val senderId: String? = null,
    val senderUsername: String? = null,
    val receiverId: String? = null,
    val body: String? = null,
    val sentAt: Long? = null
)
