package pl.firmatpp.kierowca.sync

import com.google.gson.Gson
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import java.net.URI
import java.util.concurrent.TimeUnit
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.launch
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.WebSocket
import okhttp3.WebSocketListener
import pl.firmatpp.kierowca.data.DriverRepository
import pl.firmatpp.kierowca.data.model.BroadcastAuthResponse
import pl.firmatpp.kierowca.data.model.RealtimeConfigDto
import pl.firmatpp.kierowca.diagnostics.AppDiagnostics
import pl.firmatpp.kierowca.data.ApiErrorKind
import pl.firmatpp.kierowca.data.ApiErrorMapper

interface DriverLiveSyncGateway {
    suspend fun broadcastAuth(socketId: String, channelName: String): BroadcastAuthResponse
    suspend fun storeRealtimeStatus(status: String, socketId: String?, error: String?)
}

class DriverRepositoryLiveSyncGateway(
    private val repository: DriverRepository,
) : DriverLiveSyncGateway {
    override suspend fun broadcastAuth(socketId: String, channelName: String): BroadcastAuthResponse =
        repository.broadcastAuth(socketId, channelName)

    override suspend fun storeRealtimeStatus(status: String, socketId: String?, error: String?) {
        repository.storeRealtimeStatus(status, socketId, error)
    }
}

interface LiveWebSocketFactory {
    fun newWebSocket(url: String, origin: String?, listener: WebSocketListener): WebSocket
}

class OkHttpLiveWebSocketFactory(
    private val client: OkHttpClient,
) : LiveWebSocketFactory {
    override fun newWebSocket(url: String, origin: String?, listener: WebSocketListener): WebSocket =
        client.newWebSocket(
            Request.Builder()
                .url(url)
                .header("Origin", origin?.takeIf { it.isNotBlank() } ?: websocketOrigin(url))
                .build(),
            listener,
        )
}

internal fun websocketOrigin(url: String): String =
    URI(
        url
        .replaceFirst("wss://", "https://")
        .replaceFirst("ws://", "http://")
    ).let { uri ->
        val defaultPort = (uri.scheme == "https" && uri.port == 443) || (uri.scheme == "http" && uri.port == 80)
        val port = if (uri.port > 0 && !defaultPort) ":${uri.port}" else ""
        "${uri.scheme}://${uri.host}$port"
    }

enum class LiveSyncConnectionState {
    Stopped,
    WaitingForNetwork,
    Disconnected,
    Connecting,
    Subscribing,
    Connected,
}

data class LiveSyncDiagnostics(
    val state: LiveSyncConnectionState,
    val desiredActive: Boolean,
    val networkAvailable: Boolean,
    val foregroundActive: Boolean,
    val configured: Boolean,
    val socketId: String?,
    val reconnectAttempt: Int,
    val messageVersion: Long,
    val lastMessageAtEpochMillis: Long?,
    val lastDisconnectReason: String?,
)

class DriverLiveSyncClient(
    private val gateway: DriverLiveSyncGateway,
    private val onConnected: () -> Unit,
    private val onHint: (DriverSyncHint) -> Unit,
    private val onAuthenticationFailed: () -> Unit = {},
    private val webSocketFactory: LiveWebSocketFactory = OkHttpLiveWebSocketFactory(defaultOkHttpClient()),
    private val gson: Gson = Gson(),
    private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO),
    private val reconnectDelaysMs: List<Long> = DEFAULT_RECONNECT_DELAYS_MS,
    private val staleTimeoutMs: Long = STALE_TIMEOUT_MS,
) {
    constructor(
        repository: DriverRepository,
        onConnected: () -> Unit,
        onHint: (DriverSyncHint) -> Unit,
        onAuthenticationFailed: () -> Unit = {},
        client: OkHttpClient = defaultOkHttpClient(),
        gson: Gson = Gson(),
    ) : this(
        gateway = DriverRepositoryLiveSyncGateway(repository),
        onConnected = onConnected,
        onHint = onHint,
        onAuthenticationFailed = onAuthenticationFailed,
        webSocketFactory = OkHttpLiveWebSocketFactory(client),
        gson = gson,
    )

    private companion object {
        const val HEARTBEAT_INTERVAL_MS = 10_000L
        const val STALE_TIMEOUT_MS = 30_000L
        val DEFAULT_RECONNECT_DELAYS_MS = listOf(1_000L, 2_000L, 5_000L, 10_000L, 30_000L)

        fun defaultOkHttpClient(): OkHttpClient =
            OkHttpClient.Builder()
                .pingInterval(15, TimeUnit.SECONDS)
                .build()
    }

    private val lock = Any()
    private var desiredActive: Boolean = false
    private var networkAvailable: Boolean = true
    private var state: LiveSyncConnectionState = LiveSyncConnectionState.Stopped
    private var webSocket: WebSocket? = null
    private var driverId: String? = null
    private var realtimeConfig: RealtimeConfigDto? = null
    private var socketId: String? = null
    private var reconnectJob: Job? = null
    private var heartbeatJob: Job? = null
    private var staleWatchdogJob: Job? = null
    private var reconnectAttempt: Int = 0
    private var messageVersion: Long = 0
    private var lastMessageAtEpochMillis: Long? = null
    private var lastDisconnectReason: String? = null
    private var foregroundActive: Boolean = true
    private val _connectionState = MutableStateFlow(LiveSyncConnectionState.Stopped)
    val connectionState: StateFlow<LiveSyncConnectionState> = _connectionState.asStateFlow()

    fun diagnostics(): LiveSyncDiagnostics = synchronized(lock) {
        LiveSyncDiagnostics(
            state = state,
            desiredActive = desiredActive,
            networkAvailable = networkAvailable,
            foregroundActive = foregroundActive,
            configured = realtimeConfig != null,
            socketId = socketId,
            reconnectAttempt = reconnectAttempt,
            messageVersion = messageVersion,
            lastMessageAtEpochMillis = lastMessageAtEpochMillis,
            lastDisconnectReason = lastDisconnectReason,
        )
    }

    private fun updateState(next: LiveSyncConnectionState) {
        state = next
        _connectionState.value = next
    }

    fun start(driverId: String, config: RealtimeConfigDto?) {
        if (!isConfigUsable(config)) return

        synchronized(lock) {
            realtimeConfig = config
            this.driverId = driverId
            desiredActive = true
        }
        ensureConnected()
    }

    fun ensureConnected() {
        val shouldConnect = synchronized(lock) {
            desiredActive &&
                foregroundActive &&
                networkAvailable &&
                state !in setOf(
                    LiveSyncConnectionState.Connecting,
                    LiveSyncConnectionState.Subscribing,
                    LiveSyncConnectionState.Connected,
                )
        }
        if (shouldConnect) connectNow()
    }

    fun onNetworkAvailable(available: Boolean) {
        val shouldReconnect = synchronized(lock) {
            networkAvailable = available
            if (!available) {
                reconnectJob?.cancel()
                reconnectJob = null
                closeSocketLocked("network_lost")
                updateState(if (desiredActive) LiveSyncConnectionState.WaitingForNetwork else LiveSyncConnectionState.Stopped)
                false
            } else {
                desiredActive && state == LiveSyncConnectionState.WaitingForNetwork
            }
        }

        if (shouldReconnect) connectNow(resetAttempt = true)
    }

    fun setForeground(active: Boolean) {
        val shouldConnect = synchronized(lock) {
            foregroundActive = active
            if (!active) {
                reconnectJob?.cancel()
                reconnectJob = null
                closeSocketLocked("app_background")
                updateState(LiveSyncConnectionState.Stopped)
                false
            } else {
                desiredActive && networkAvailable
            }
        }
        if (shouldConnect) connectNow(resetAttempt = true)
    }

    fun stop() {
        synchronized(lock) {
            desiredActive = false
            reconnectJob?.cancel()
            reconnectJob = null
            closeSocketLocked("client_stop")
            driverId = null
            realtimeConfig = null
            socketId = null
            reconnectAttempt = 0
            updateState(LiveSyncConnectionState.Stopped)
        }
        reportRealtimeStatus("disconnected", "client_stop")
    }

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

    private fun connectNow(resetAttempt: Boolean = false) {
        val (wsUrl, origin) = synchronized(lock) {
            val config = realtimeConfig ?: return
            val id = driverId ?: return
            if (!desiredActive || !foregroundActive || !networkAvailable || !isConfigUsable(config)) return
            if (state == LiveSyncConnectionState.Connecting || state == LiveSyncConnectionState.Subscribing || state == LiveSyncConnectionState.Connected) return

            if (resetAttempt) reconnectAttempt = 0
            reconnectJob?.cancel()
            reconnectJob = null
            socketId = null
            updateState(LiveSyncConnectionState.Connecting)

            val appKey = config.reverbAppKey.orEmpty()
            val wsBaseUrl = config.reverbWsBaseUrl.orEmpty()
            Pair(
                wsBaseUrl.trimEnd('/') + "/" + appKey + "?protocol=7&client=android&version=1.0&flash=false",
                config.reverbOrigin,
            )
        }

        val socket = webSocketFactory.newWebSocket(
            wsUrl,
            origin,
            object : WebSocketListener() {
                override fun onMessage(webSocket: WebSocket, text: String) {
                    handleMessage(webSocket, text)
                }

                override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
                    handleDisconnect(
                        status = "disconnected",
                        reason = reason.takeIf { it.isNotBlank() } ?: "closed",
                        socket = webSocket,
                    )
                }

                override fun onFailure(webSocket: WebSocket, t: Throwable, response: okhttp3.Response?) {
                    AppDiagnostics.log("realtime_error: ${t.message ?: response?.message ?: "unknown"}")
                    handleDisconnect(
                        status = "error",
                        reason = t.message ?: response?.message ?: "failure",
                        socket = webSocket,
                    )
                }
            },
        )

        synchronized(lock) {
            webSocket = socket
        }
    }

    private fun handleMessage(socket: WebSocket, text: String) {
        val isCurrentSocket = synchronized(lock) {
            if (webSocket !== socket) {
                false
            } else {
                messageVersion += 1
                lastMessageAtEpochMillis = System.currentTimeMillis()
                true
            }
        }
        if (!isCurrentSocket) return

        val root = runCatching { JsonParser.parseString(text).asJsonObject }.getOrNull() ?: return
        val event = root.string("event") ?: return

        when (event) {
            "pusher:ping" -> {
                socket.send("""{"event":"pusher:pong","data":{}}""")
                restartStaleWatchdogIfConnected()
            }
            "pusher:connection_established" -> {
                val nextSocketId = root.dataObject()?.string("socket_id") ?: return
                synchronized(lock) {
                    socketId = nextSocketId
                    updateState(LiveSyncConnectionState.Subscribing)
                }
                subscribe(socket, nextSocketId)
            }
            "pusher_internal:subscription_succeeded" -> {
                val channel = root.string("channel")
                val id = synchronized(lock) { driverId } ?: return
                if (channel == "private-driver-mobile.$id") {
                    synchronized(lock) {
                        reconnectAttempt = 0
                        updateState(LiveSyncConnectionState.Connected)
                    }
                    reportRealtimeStatus("connected")
                    startHeartbeat()
                    startStaleWatchdog()
                    onConnected()
                }
            }
            "DriverMobileSyncHint" -> {
                restartStaleWatchdogIfConnected()
                parseHint(root.dataObject())?.let(onHint)
            }
            else -> restartStaleWatchdogIfConnected()
        }
    }

    private fun subscribe(socket: WebSocket, socketId: String) {
        val id = synchronized(lock) { driverId } ?: return
        val channels = listOf("private-driver-mobile.$id", "private-driver-mobile.0")

        scope.launch {
            runCatching {
                channels.forEach { channel ->
                    val auth = gateway.broadcastAuth(socketId, channel).auth
                    if (!isCurrentSocket(socket)) return@launch
                    val payload = mapOf(
                        "event" to "pusher:subscribe",
                        "data" to mapOf(
                            "channel" to channel,
                            "auth" to auth,
                        ),
                    )
                    check(socket.send(gson.toJson(payload))) { "WebSocket send returned false" }
                }
            }.onFailure { throwable ->
                AppDiagnostics.log("realtime_subscription_error: ${throwable.message ?: throwable::class.java.simpleName}")
                if (ApiErrorMapper.map(throwable).kind == ApiErrorKind.Auth) {
                    handleAuthenticationFailure(socket)
                    return@onFailure
                }
                socket.close(1000, "subscription_failed")
                handleDisconnect("error", "subscription_failed", socket)
            }
        }
    }

    private fun handleDisconnect(status: String, reason: String?, socket: WebSocket? = null) {
        val shouldSchedule = synchronized(lock) {
            if (socket != null && webSocket !== socket) return

            stopHeartbeatLocked()
            stopStaleWatchdogLocked()
            webSocket = null
            socketId = null
            lastDisconnectReason = reason

            if (!desiredActive) {
                updateState(LiveSyncConnectionState.Stopped)
                false
            } else if (!networkAvailable) {
                updateState(LiveSyncConnectionState.WaitingForNetwork)
                false
            } else {
                updateState(LiveSyncConnectionState.Disconnected)
                true
            }
        }

        reportRealtimeStatus(status, reason)
        if (shouldSchedule) scheduleReconnect()
    }

    private fun scheduleReconnect() {
        val delayMs = synchronized(lock) {
            if (!desiredActive || !foregroundActive || !networkAvailable || state == LiveSyncConnectionState.Stopped) return
            if (reconnectJob?.isActive == true) return
            val delay = reconnectDelaysMs.getOrElse(reconnectAttempt) { reconnectDelaysMs.last() }
            reconnectAttempt += 1
            delay
        }

        reconnectJob = scope.launch {
            delay(delayMs)
            connectNow()
        }
    }

    private fun startHeartbeat() {
        synchronized(lock) {
            heartbeatJob?.cancel()
            heartbeatJob = scope.launch {
                while (true) {
                    reportRealtimeStatus("connected")
                    delay(HEARTBEAT_INTERVAL_MS)
                }
            }
        }
    }

    private fun startStaleWatchdog() {
        synchronized(lock) {
            stopStaleWatchdogLocked()
            val observedVersion = messageVersion
            staleWatchdogJob = scope.launch {
                delay(staleTimeoutMs)
                val socketToClose = synchronized(lock) {
                    if (state == LiveSyncConnectionState.Connected && messageVersion == observedVersion) {
                        webSocket
                    } else {
                        null
                    }
                }

                if (socketToClose != null) {
                    socketToClose.close(1001, "stale_connection")
                    handleDisconnect("error", "stale_connection", socketToClose)
                    ensureConnected()
                }
            }
        }
    }

    private fun restartStaleWatchdogIfConnected() {
        val connected = synchronized(lock) { state == LiveSyncConnectionState.Connected }
        if (connected) startStaleWatchdog()
    }

    private fun isCurrentSocket(socket: WebSocket): Boolean =
        synchronized(lock) { webSocket === socket }

    private fun closeSocketLocked(reason: String) {
        stopHeartbeatLocked()
        stopStaleWatchdogLocked()
        webSocket?.close(1000, reason)
        webSocket = null
        socketId = null
    }

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

    private fun stopStaleWatchdogLocked() {
        staleWatchdogJob?.cancel()
        staleWatchdogJob = null
    }

    private fun reportRealtimeStatus(status: String, error: String? = null) {
        scope.launch {
            runCatching {
                gateway.storeRealtimeStatus(status, socketId, error)
            }.onFailure { throwable ->
                if (ApiErrorMapper.map(throwable).kind == ApiErrorKind.Auth) handleAuthenticationFailure()
            }
        }
    }

    private fun handleAuthenticationFailure(socket: WebSocket? = null) {
        val notify = synchronized(lock) {
            if (socket != null && webSocket !== socket) return
            val wasActive = desiredActive
            desiredActive = false
            reconnectJob?.cancel()
            reconnectJob = null
            closeSocketLocked("authentication_failed")
            updateState(LiveSyncConnectionState.Stopped)
            wasActive
        }
        if (notify) onAuthenticationFailed()
    }

    private fun isConfigUsable(config: RealtimeConfigDto?): Boolean =
        config?.reverbEnabled == true &&
            !config.reverbAppKey.isNullOrBlank() &&
            !config.reverbWsBaseUrl.isNullOrBlank()

    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
}
