package ru.yandex.galois.clients.idm

import com.fasterxml.jackson.databind.ObjectMapper
import io.github.resilience4j.kotlin.retry.executeSuspendFunction
import io.github.resilience4j.retry.Retry
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.beans.factory.annotation.Value
import org.springframework.http.HttpHeaders
import org.springframework.stereotype.Component
import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.reactive.function.client.awaitEntity
import org.springframework.web.reactive.function.client.awaitExchange
import org.springframework.web.reactive.function.client.createExceptionAndAwait
import org.springframework.web.util.UriComponentsBuilder
import ru.yandex.galois.clients.idm.model.IdmGroup
import ru.yandex.galois.clients.idm.model.IdmResponse
import ru.yandex.galois.clients.utils.ClientFactory
import java.util.concurrent.atomic.AtomicReference

@Component
class IdmClient(@Value("\${idm.client.connectTimeoutMs}") connectTimeoutMillis: Int,
    @Value("\${idm.client.readTimeoutMs}") readTimeoutMillis: Long,
    @Value("\${idm.client.writeTimeoutMs}") writeTimeoutMillis: Long,
    @Value("\${idm.client.sslHandshakeTimeoutMs}") sslHandshakeTimeoutMillis: Long,
    @Value("\${idm.client.sslCloseNotifyReadTimeoutMs}") sslCloseNotifyReadTimeoutMillis: Long,
    @Value("\${idm.client.sslCloseFlushTimeoutMs}") sslCloseFlushTimeoutMillis: Long,
    @Value("\${idm.client.responseTimeoutMs}") responseTimeoutMillis: Long,
    @Value("\${idm.client.baseUrl}") private val baseUrl: String,
    @Value("\${idm.client.maxResponseBytes}") maxResponseBytes: Long,
    @Value("\${http.client.userAgent}") private val userAgent: String,
    @Qualifier("httpClientObjectMapper") objectMapper: ObjectMapper,
    @Qualifier("httpRetry") private val retry: Retry
) {

    private val client: WebClient

    init {
        client = ClientFactory.prepareClient(connectTimeoutMillis, readTimeoutMillis, writeTimeoutMillis,
            sslHandshakeTimeoutMillis, sslCloseNotifyReadTimeoutMillis, sslCloseFlushTimeoutMillis,
            responseTimeoutMillis, objectMapper, maxResponseBytes)
    }

    suspend fun getGroups(oauthToken: String, type: String? = null, limit: Long? = null,
        offset: Long? = null): IdmResponse<IdmGroup> {
        val uriBuilder = UriComponentsBuilder
            .fromHttpUrl(baseUrl)
            .path("/groups/")
        limit?.let {uriBuilder.queryParam("limit", it)}
        offset?.let { uriBuilder.queryParam("offset", it) }
        type?.let { uriBuilder.queryParam("type", it) }
        val uri = uriBuilder.build().toUri()
        return retry.executeSuspendFunction {
            client.get()
                .uri(uri)
                .header("Authorization", "OAuth $oauthToken")
                .header(HttpHeaders.USER_AGENT, userAgent)
                .awaitExchange { response ->
                    if (response.statusCode().is2xxSuccessful) {
                        response.awaitEntity<IdmResponse<IdmGroup>>().body
                    } else {
                        throw response.createExceptionAndAwait()
                    }
                }
        }
    }

    suspend fun getAllGroups(oauthToken: String, type: String? = null,
        pageSize: Long? = 1000): Flow<List<IdmGroup>> = flow {
        val firstPage = getGroups(oauthToken = oauthToken, type = type, limit = pageSize)
        if (firstPage.objects.isNotEmpty()) {
            emit(firstPage.objects)
        }
        val lastMeta = AtomicReference(firstPage.meta)
        while (true) {
            val nextOffset = nextOffset(lastMeta.get().next) ?: break
            val nextPage = getGroups(oauthToken = oauthToken, type = type, limit = pageSize, offset = nextOffset)
            if (nextPage.objects.isNotEmpty()) {
                emit(nextPage.objects)
            }
            lastMeta.set(nextPage.meta)
        }
    }

    private fun nextOffset(nextUrl: String?): Long? {
        return UriComponentsBuilder.fromUriString(nextUrl ?: return null).build()
            .queryParams.getFirst("offset")?.toLongOrNull()
    }

}
