package ru.yandex.galois.clients.abc

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.abc.model.AbcResponse
import ru.yandex.galois.clients.abc.model.AbcRole
import ru.yandex.galois.clients.abc.model.AbcService
import ru.yandex.galois.clients.abc.model.AbcServiceMember
import ru.yandex.galois.clients.utils.ClientFactory
import java.util.concurrent.atomic.AtomicReference

@Component
class AbcClient(@Value("\${abc.client.connectTimeoutMs}") connectTimeoutMillis: Int,
    @Value("\${abc.client.readTimeoutMs}") readTimeoutMillis: Long,
    @Value("\${abc.client.writeTimeoutMs}") writeTimeoutMillis: Long,
    @Value("\${abc.client.sslHandshakeTimeoutMs}") sslHandshakeTimeoutMillis: Long,
    @Value("\${abc.client.sslCloseNotifyReadTimeoutMs}") sslCloseNotifyReadTimeoutMillis: Long,
    @Value("\${abc.client.sslCloseFlushTimeoutMs}") sslCloseFlushTimeoutMillis: Long,
    @Value("\${abc.client.responseTimeoutMs}") responseTimeoutMillis: Long,
    @Value("\${abc.client.baseUrl}") private val baseUrl: String,
    @Value("\${abc.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 getServices(oauthToken: String, cursor: String? = null, pageSize: Long? = null,
        ordering: String? = null, withNonExportable: Boolean = false): AbcResponse<AbcService> {
        val fields = listOf("id", "created_at", "slug", "state", "readonly_state", "type", "is_exportable")
            .joinToString(separator = ",")
        val uriBuilder = UriComponentsBuilder
            .fromHttpUrl(baseUrl)
            .path("/services/")
            .queryParam("fields", fields)
        cursor?.let { uriBuilder.queryParam("cursor", it) }
        pageSize?.let { uriBuilder.queryParam("page_size", it) }
        ordering?.let { uriBuilder.queryParam("ordering", it) }
        if (withNonExportable) {
            uriBuilder.queryParam("is_exportable__in", "true,false")
        }
        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<AbcResponse<AbcService>>().body
                    } else {
                        throw response.createExceptionAndAwait()
                    }
                }
        }
    }

    suspend fun getRoles(oauthToken: String, cursor: String? = null, pageSize: Long? = null,
        ordering: String? = null): AbcResponse<AbcRole> {
        val fields = listOf("id", "created_at", "code", "service", "scope")
            .joinToString(separator = ",")
        val uriBuilder = UriComponentsBuilder
            .fromHttpUrl(baseUrl)
            .path("/roles/")
            .queryParam("fields", fields)
        cursor?.let {uriBuilder.queryParam("cursor", it)}
        pageSize?.let { uriBuilder.queryParam("page_size", it) }
        ordering?.let { uriBuilder.queryParam("ordering", 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<AbcResponse<AbcRole>>().body
                    } else {
                        throw response.createExceptionAndAwait()
                    }
                }
        }
    }

    suspend fun getServiceMembers(oauthToken: String, cursor: String? = null, pageSize: Long? = null,
        ordering: String? = null, withNonExportableServices: Boolean = false): AbcResponse<AbcServiceMember> {
        val fields = listOf("id", "person", "service", "role", "created_at", "modified_at")
            .joinToString(separator = ",")
        val uriBuilder = UriComponentsBuilder
            .fromHttpUrl(baseUrl)
            .path("/services/members/")
            .queryParam("fields", fields)
        cursor?.let {uriBuilder.queryParam("cursor", it)}
        pageSize?.let { uriBuilder.queryParam("page_size", it) }
        ordering?.let { uriBuilder.queryParam("ordering", it) }
        if (withNonExportableServices) {
            uriBuilder.queryParam("service__is_exportable__in", "true,false")
        }
        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<AbcResponse<AbcServiceMember>>().body
                    } else {
                        throw response.createExceptionAndAwait()
                    }
                }
        }
    }

    suspend fun getAllServices(oauthToken: String, pageSize: Long? = 1000, ordering: String? = null,
        withNonExportable: Boolean = false): Flow<List<AbcService>> = flow {
        val firstPage = getServices(oauthToken = oauthToken, pageSize = pageSize, ordering = ordering,
            withNonExportable = withNonExportable)
        if (firstPage.results.isNotEmpty()) {
            emit(firstPage.results)
        }
        val lastNext = AtomicReference(firstPage.next)
        while (true) {
            val nextCursor = nextCursor(lastNext.get()) ?: break
            val nextPage = getServices(oauthToken = oauthToken, cursor = nextCursor, pageSize = pageSize,
                ordering = ordering, withNonExportable = withNonExportable)
            if (nextPage.results.isNotEmpty()) {
                emit(nextPage.results)
            }
            lastNext.set(nextPage.next)
        }
    }

    suspend fun getAllRoles(oauthToken: String, pageSize: Long? = 1000,
        ordering: String? = null): Flow<List<AbcRole>> = flow {
        val firstPage = getRoles(oauthToken = oauthToken, pageSize = pageSize, ordering = ordering)
        if (firstPage.results.isNotEmpty()) {
            emit(firstPage.results)
        }
        val lastNext = AtomicReference(firstPage.next)
        while (true) {
            val nextCursor = nextCursor(lastNext.get()) ?: break
            val nextPage = getRoles(oauthToken = oauthToken, cursor = nextCursor, pageSize = pageSize,
                ordering = ordering)
            if (nextPage.results.isNotEmpty()) {
                emit(nextPage.results)
            }
            lastNext.set(nextPage.next)
        }
    }

    suspend fun getAllServiceMembers(oauthToken: String, pageSize: Long? = 1000, ordering: String? = null,
        withNonExportableServices: Boolean = false): Flow<List<AbcServiceMember>> = flow {
        val firstPage = getServiceMembers(oauthToken = oauthToken, pageSize = pageSize, ordering = ordering,
            withNonExportableServices = withNonExportableServices)
        if (firstPage.results.isNotEmpty()) {
            emit(firstPage.results)
        }
        val lastNext = AtomicReference(firstPage.next)
        while (true) {
            val nextCursor = nextCursor(lastNext.get()) ?: break
            val nextPage = getServiceMembers(oauthToken = oauthToken, cursor = nextCursor, pageSize = pageSize,
                ordering = ordering, withNonExportableServices = withNonExportableServices)
            if (nextPage.results.isNotEmpty()) {
                emit(nextPage.results)
            }
            lastNext.set(nextPage.next)
        }
    }

    private fun nextCursor(nextUrl: String?): String? {
        return UriComponentsBuilder.fromHttpUrl(nextUrl ?: return null).build()
            .queryParams.getFirst("cursor")
    }

}
