package pl.firmatpp.kierowca.sync

import com.google.gson.Gson
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import java.util.concurrent.atomic.AtomicBoolean
import kotlinx.coroutines.Job
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.WebSocket
import okhttp3.WebSocketListener
import pl.firmatpp.kierowca.BuildConfig
import pl.firmatpp.kierowca.data.DriverRepository

class DriverLiveSyncClient(
    private val repository: DriverRepository,
    private val onConnected: () -> Unit,
    private val onHint: (DriverSyncHint) -> Unit,
    private val client: OkHttpClient = OkHttpClient(),
    private val gson: Gson = Gson(),
) {
    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
    private val started = AtomicBoolean(false)
    private var webSocket: WebSocket? = null
    private var driverId: String? = null
    private var socketId: String? = null
    private var heartbeatJob: Job? = null

    fun start(driverId: String) {
        if (BuildConfig.REVERB_APP_KEY.isBlank()) return
        this.driverId = driverId
        if (!started.compareAndSet(false, true)) return

        val wsUrl = BuildConfig.REVERB_WS_BASE_URL.trimEnd('/') +
            "/" + BuildConfig.REVERB_APP_KEY +
            "?protocol=7&client=android&version=1.0&flash=false"

        webSocket = client.newWebSocket(
            Request.Builder().url(wsUrl).build(),
            object : WebSocketListener() {
                override fun onMessage(webSocket: WebSocket, text: String) {
                    handleMessage(webSocket, text)
                }

                override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
                    stopHeartbeat()
                    reportRealtimeStatus("disconnected", reason.takeIf { it.isNotBlank() })
                    started.set(false)
                    scheduleReconnect()
                }

                override fun onFailure(webSocket: WebSocket, t: Throwable, response: okhttp3.Response?) {
                    stopHeartbeat()
                    reportRealtimeStatus("error", t.message ?: response?.message)
                    started.set(false)
                    scheduleReconnect()
                }
            },
        )
    }

    fun stop() {
        started.set(false)
        stopHeartbeat()
        reportRealtimeStatus("disconnected", "client_stop")
        webSocket?.close(1000, "logout")
        webSocket = null
        driverId = null
        socketId = null
    }

    fun close() {
        stop()
        scope.cancel()
    }

    private fun handleMessage(socket: WebSocket, text: String) {
        val root = runCatching { JsonParser.parseString(text).asJsonObject }.getOrNull() ?: return
        val event = root.string("event") ?: return

        when (event) {
            "pusher:connection_established" -> {
                socketId = root.dataObject()?.string("socket_id") ?: return
                subscribe(socket, socketId ?: return)
            }
            "pusher_internal:subscription_succeeded" -> {
                val channel = root.string("channel")
                val id = driverId ?: return
                if (channel == "private-driver-mobile.$id") {
                    reportRealtimeStatus("connected")
                    startHeartbeat()
                    onConnected()
                }
            }
            "DriverMobileSyncHint" -> parseHint(root.dataObject())?.let(onHint)
        }
    }

    private fun subscribe(socket: WebSocket, socketId: String) {
        val id = driverId ?: return
        val channel = "private-driver-mobile.$id"

        scope.launch {
            runCatching {
                val auth = repository.broadcastAuth(socketId, channel).auth
                val payload = mapOf(
                    "event" to "pusher:subscribe",
                    "data" to mapOf(
                        "channel" to channel,
                        "auth" to auth,
                    ),
                )
                socket.send(gson.toJson(payload))
            }
        }
    }

    private fun parseHint(data: JsonObject?): DriverSyncHint? {
        if (data == null || data.string("type") != "driver_sync_hint") return null

        return DriverSyncHint(
            scope = data.string("scope") ?: return null,
            date = data.string("date"),
            routeId = data.string("routeId"),
            version = data.string("version")?.toLongOrNull() ?: data.get("version")?.asLong ?: return null,
            checksum = data.string("checksum") ?: return null,
        )
    }

    private fun JsonObject.dataObject(): JsonObject? {
        val data = get("data") ?: return null
        return if (data.isJsonObject) data.asJsonObject else runCatching { JsonParser.parseString(data.asString).asJsonObject }.getOrNull()
    }

    private fun JsonObject.string(name: String): String? =
        get(name)?.takeIf { !it.isJsonNull }?.asString

    private fun scheduleReconnect() {
        val id = driverId ?: return
        scope.launch {
            delay(5_000)
            if (!started.get() && driverId == id) {
                start(id)
            }
        }
    }

    private fun startHeartbeat() {
        heartbeatJob?.cancel()
        heartbeatJob = scope.launch {
            while (started.get()) {
                reportRealtimeStatus("connected")
                delay(30_000)
            }
        }
    }

    private fun stopHeartbeat() {
        heartbeatJob?.cancel()
        heartbeatJob = null
    }

    private fun reportRealtimeStatus(status: String, error: String? = null) {
        scope.launch {
            runCatching {
                repository.storeRealtimeStatus(status, socketId, error)
            }
        }
    }
}
