package com.seven.ikar.kotlin.core.realtime

import com.microsoft.signalr.HubConnection
import com.microsoft.signalr.HubConnectionBuilder
import com.seven.ikar.kotlin.core.model.AndroidAppReleaseDto
import com.seven.ikar.kotlin.core.model.CallAnswerDto
import com.seven.ikar.kotlin.core.model.CallConnectedDto
import com.seven.ikar.kotlin.core.model.CallEndedDto
import com.seven.ikar.kotlin.core.model.CallIceCandidateDto
import com.seven.ikar.kotlin.core.model.CallMediaType
import com.seven.ikar.kotlin.core.model.CallOfferDto
import com.seven.ikar.kotlin.core.model.ChatSummaryDto
import com.seven.ikar.kotlin.core.model.IncomingCallDto
import com.seven.ikar.kotlin.core.model.MessageDto
import com.seven.ikar.kotlin.core.model.OutgoingCallDto
import com.seven.ikar.kotlin.core.model.TypingIndicatorDto
import com.seven.ikar.kotlin.core.network.ClientVersionProvider
import com.seven.ikar.kotlin.core.network.ServerConfig
import io.reactivex.rxjava3.core.Single
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
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.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.net.URLEncoder
import java.nio.charset.StandardCharsets

enum class RealtimeConnectionStage {
    Disconnected,
    Connecting,
    Connected,
    Reconnecting
}

class RealtimeService(
    private val clientVersionProvider: ClientVersionProvider
) {
    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
    private val mutex = Mutex()
    private var connection: HubConnection? = null
    private var accessToken: String? = null
    private var reconnectJob: Job? = null

    private val _stage = MutableStateFlow(RealtimeConnectionStage.Disconnected)
    val stage: StateFlow<RealtimeConnectionStage> = _stage

    private val _messagesCreated = MutableSharedFlow<MessageDto>(extraBufferCapacity = 32)
    val messagesCreated: SharedFlow<MessageDto> = _messagesCreated

    private val _messagesUpdated = MutableSharedFlow<MessageDto>(extraBufferCapacity = 32)
    val messagesUpdated: SharedFlow<MessageDto> = _messagesUpdated

    private val _typingChanged = MutableSharedFlow<TypingIndicatorDto>(extraBufferCapacity = 64)
    val typingChanged: SharedFlow<TypingIndicatorDto> = _typingChanged

    private val _chatCreated = MutableSharedFlow<ChatSummaryDto>(extraBufferCapacity = 16)
    val chatCreated: SharedFlow<ChatSummaryDto> = _chatCreated

    private val _chatsChanged = MutableSharedFlow<Unit>(extraBufferCapacity = 8)
    val chatsChanged: SharedFlow<Unit> = _chatsChanged

    private val _appUpdateAvailable = MutableSharedFlow<AndroidAppReleaseDto>(extraBufferCapacity = 8)
    val appUpdateAvailable: SharedFlow<AndroidAppReleaseDto> = _appUpdateAvailable

    private val _incomingCall = MutableSharedFlow<IncomingCallDto>(extraBufferCapacity = 8)
    val incomingCall: SharedFlow<IncomingCallDto> = _incomingCall

    private val _callConnected = MutableSharedFlow<CallConnectedDto>(extraBufferCapacity = 8)
    val callConnected: SharedFlow<CallConnectedDto> = _callConnected

    private val _callEnded = MutableSharedFlow<CallEndedDto>(extraBufferCapacity = 8)
    val callEnded: SharedFlow<CallEndedDto> = _callEnded

    private val _callOffer = MutableSharedFlow<CallOfferDto>(extraBufferCapacity = 16)
    val callOffer: SharedFlow<CallOfferDto> = _callOffer

    private val _callAnswer = MutableSharedFlow<CallAnswerDto>(extraBufferCapacity = 16)
    val callAnswer: SharedFlow<CallAnswerDto> = _callAnswer

    private val _callIceCandidate = MutableSharedFlow<CallIceCandidateDto>(extraBufferCapacity = 32)
    val callIceCandidate: SharedFlow<CallIceCandidateDto> = _callIceCandidate

    fun connect(accessToken: String) {
        this.accessToken = accessToken
        scope.launch {
            ensureConnected()
        }
    }

    suspend fun ensureConnected(accessToken: String) {
        this.accessToken = accessToken
        ensureConnected()
        check(_stage.value == RealtimeConnectionStage.Connected) {
            "Realtime hub is not connected."
        }
    }

    fun disconnect() {
        scope.launch {
            mutex.withLock {
                accessToken = null
                reconnectJob?.cancel()
                reconnectJob = null
                connection?.stop()?.blockingAwait()
                connection = null
                _stage.value = RealtimeConnectionStage.Disconnected
            }
        }
    }

    fun startCall(chatId: String, mediaType: CallMediaType): OutgoingCallDto {
        val activeConnection = requireConnectedConnection()
        return if (mediaType == CallMediaType.Audio) {
            activeConnection.invoke(RealtimeOutgoingCallDto::class.java, "StartCall", chatId).blockingGet().toCore()
        } else {
            activeConnection.invoke(RealtimeOutgoingCallDto::class.java, "StartMediaCall", chatId, mediaType.wireValue).blockingGet().toCore()
        }
    }

    fun acceptCall(callId: String) {
        requireConnectedConnection().invoke("AcceptCall", callId).blockingAwait()
    }

    fun declineCall(callId: String) {
        requireConnectedConnection().invoke("DeclineCall", callId).blockingAwait()
    }

    fun endCall(callId: String) {
        requireConnectedConnection().invoke("EndCall", callId).blockingAwait()
    }

    fun sendCallOffer(payload: CallOfferDto) {
        requireConnectedConnection().send("SendCallOffer", payload.toRealtime())
    }

    fun sendCallAnswer(payload: CallAnswerDto) {
        requireConnectedConnection().send("SendCallAnswer", payload.toRealtime())
    }

    fun sendCallIceCandidate(payload: CallIceCandidateDto) {
        requireConnectedConnection().send("SendCallIceCandidate", payload.toRealtime())
    }

    fun sendTyping(chatId: String, isTyping: Boolean) {
        requireConnectedConnection().send("SendTyping", chatId, isTyping)
    }

    private suspend fun ensureConnected() {
        mutex.withLock {
            val current = connection
            if (current != null && current.connectionState == com.microsoft.signalr.HubConnectionState.CONNECTED) {
                return
            }

            if (accessToken.isNullOrBlank()) {
                _stage.value = RealtimeConnectionStage.Disconnected
                return
            }

            _stage.value = if (current == null) {
                RealtimeConnectionStage.Connecting
            } else {
                RealtimeConnectionStage.Reconnecting
            }

            val newConnection = buildConnection(accessToken!!)
            connection = newConnection
            runCatching {
                newConnection.start().blockingAwait()
            }.onSuccess {
                _stage.value = RealtimeConnectionStage.Connected
            }.onFailure {
                if (connection == newConnection) {
                    connection = null
                }
                runCatching { newConnection.stop().blockingAwait() }
                _stage.value = RealtimeConnectionStage.Disconnected
                scheduleReconnect()
            }
        }
    }

    private fun buildConnection(accessToken: String): HubConnection {
        val hubUrl = buildHubUrl()
        val hub = HubConnectionBuilder.create(hubUrl)
            .withAccessTokenProvider(Single.defer { Single.just(accessToken) })
            .withHeader("X-Ikar-Client-Platform", clientVersionProvider.platform)
            .withHeader("X-Ikar-Client-Version-Code", clientVersionProvider.versionCode)
            .withHeader("X-Ikar-Client-Version-Name", clientVersionProvider.versionName)
            .build()

        hub.on("MessageCreated", { payload: RealtimeMessageDto ->
            scope.launch { _messagesCreated.emit(payload.toCore()) }
        }, RealtimeMessageDto::class.java)

        hub.on("MessageUpdated", { payload: RealtimeMessageDto ->
            scope.launch { _messagesUpdated.emit(payload.toCore()) }
        }, RealtimeMessageDto::class.java)

        hub.on("TypingChanged", { payload: TypingIndicatorDto ->
            scope.launch { _typingChanged.emit(payload) }
        }, TypingIndicatorDto::class.java)

        hub.on("ChatCreated", { payload: RealtimeChatSummaryDto ->
            scope.launch { _chatCreated.emit(payload.toCore()) }
        }, RealtimeChatSummaryDto::class.java)

        hub.on("ChatsChanged", {
            scope.launch { _chatsChanged.emit(Unit) }
        })

        hub.on("AppUpdateAvailable", { payload: AndroidAppReleaseDto ->
            scope.launch { _appUpdateAvailable.emit(payload) }
        }, AndroidAppReleaseDto::class.java)

        hub.on("IncomingCall", { payload: RealtimeIncomingCallDto ->
            scope.launch { _incomingCall.emit(payload.toCore()) }
        }, RealtimeIncomingCallDto::class.java)

        hub.on("CallConnected", { payload: RealtimeCallConnectedDto ->
            scope.launch { _callConnected.emit(payload.toCore()) }
        }, RealtimeCallConnectedDto::class.java)

        hub.on("CallEnded", { payload: RealtimeCallEndedDto ->
            scope.launch { _callEnded.emit(payload.toCore()) }
        }, RealtimeCallEndedDto::class.java)

        hub.on("CallOffer", { payload: RealtimeCallOfferDto ->
            scope.launch { _callOffer.emit(payload.toCore()) }
        }, RealtimeCallOfferDto::class.java)

        hub.on("CallAnswer", { payload: RealtimeCallAnswerDto ->
            scope.launch { _callAnswer.emit(payload.toCore()) }
        }, RealtimeCallAnswerDto::class.java)

        hub.on("CallIceCandidate", { payload: RealtimeCallIceCandidateDto ->
            scope.launch { _callIceCandidate.emit(payload.toCore()) }
        }, RealtimeCallIceCandidateDto::class.java)

        hub.onClosed {
            _stage.value = RealtimeConnectionStage.Disconnected
            if (!accessToken.isNullOrBlank()) {
                scheduleReconnect()
            }
        }

        return hub
    }

    private fun scheduleReconnect() {
        if (accessToken.isNullOrBlank()) {
            return
        }

        if (reconnectJob?.isActive == true) {
            return
        }

        reconnectJob = scope.launch {
            while (accessToken != null) {
                delay(5_000)
                ensureConnected()
                if (_stage.value == RealtimeConnectionStage.Connected) {
                    return@launch
                }
            }
        }
    }

    private fun buildHubUrl(): String {
        val query = listOf(
            "clientPlatform=${encode(clientVersionProvider.platform)}",
            "clientVersionCode=${encode(clientVersionProvider.versionCode)}",
            "clientVersionName=${encode(clientVersionProvider.versionName)}"
        ).joinToString("&")
        return "${ServerConfig.absoluteMessengerUrl(ServerConfig.HubPath)}?$query"
    }

    private fun encode(value: String): String =
        URLEncoder.encode(value, StandardCharsets.UTF_8.toString())

    private fun requireConnectedConnection(): HubConnection {
        val activeConnection = connection
        check(activeConnection != null && activeConnection.connectionState == com.microsoft.signalr.HubConnectionState.CONNECTED) {
            "Realtime hub is not connected."
        }

        return activeConnection
    }
}
