package moe.lava.neon.api.gateway

import co.touchlab.kermit.Logger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
import kotlinx.serialization.ExperimentalSerializationApi
import moe.lava.neon.api.gateway.handlers.Handler
import kotlin.math.pow
import kotlin.reflect.KClass
import kotlin.time.Duration.Companion.seconds

class GatewayHandler {
    private val mEvents = MutableSharedFlow<Event.Dispatch>()
    val events = mEvents.asSharedFlow()

    private val logger = Logger.withTag("neon.core.api.gateway/handler")
    private val scope = CoroutineScope(Dispatchers.IO)
    private var session: GatewaySession? = null
    private var resumeProps: ResumeProperties? = null

    private var retryAttempts: Int = 0

    @OptIn(ExperimentalSerializationApi::class)
    suspend fun connect(token: String) {
        if (session != null) {
            logger.w(Throwable()) { "Attempted to connect, but client already connected, ignoring..." }
            return
        }

        session = GatewaySession.start(
            token = token,
            resumeProps = resumeProps,
            onSuccess = {
                logger.d { "Successful session start" }
                retryAttempts = 0
            },
            onDispatch = { scope.launch { mEvents.emit(it) } },
            onDestroy = { reason, resumeProps ->
                session = null

                if (reason is GatewayCloseReason.KeepDisconnected) {
                    this.resumeProps = resumeProps
                }

                if (reason is GatewayCloseReason.ShouldReconnect) {
                    if (reason.resume) {
                        this.resumeProps = resumeProps
                    } else {
                        this.resumeProps = null
                    }
                    scope.launch {
                        var res: Result<Unit>
                        do {
                            val dur = 2.0.pow(retryAttempts).seconds
                            logger.d { "Reconnecting in ${dur.inWholeMilliseconds}ms" }
                            delay(dur)
                            retryAttempts += 1
                            res = runCatching { connect(token) }
                            res.exceptionOrNull()?.let {
                                logger.e(it) { "Reconnect failed" }
                            }
                        } while(res.isFailure)
                    }
                }
            }
        )
    }

    suspend fun disconnect() {
        val session = session
            ?: throw IllegalStateException("Tried disconnecting with no session")
        session.close(GatewayCloseReason.ClientPaused)
    }
}
