package moe.lava.neon.core.api.gateway

import co.touchlab.kermit.Logger
import dev.zacsweers.metro.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.serialization.ExperimentalSerializationApi
import moe.lava.neon.core.di.EventHandlerGraph
import moe.lava.neon.core.repository.AuthRepository
import kotlin.math.pow
import kotlin.time.Duration.Companion.seconds

@Inject
class GatewayHandler(
    private val auth: AuthRepository,
    private val handlers: EventHandlerGraph,
) {
    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() {
        if (session != null) {
            logger.w(Throwable()) { "Attempted to connect, but client already connected, ignoring..." }
            return
        }
        val token = auth.token
            ?: throw IllegalStateException("Tried to connect to gateway with no token")

        session = GatewaySession.start(
            token = token,
            eventHandlers = handlers,
            resumeProps = resumeProps,
            onSuccess = {
                logger.d { "Successful session start" }
                retryAttempts = 0
            },
            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() }
                            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)
    }
}
