package space.nstart.pcp.slots_service.service

import com.fasterxml.jackson.databind.ObjectMapper
import org.springframework.beans.factory.ObjectProvider
import org.springframework.beans.factory.annotation.Value
import org.springframework.http.HttpStatusCode
import org.springframework.stereotype.Service
import org.springframework.web.reactive.function.client.ClientResponse
import org.springframework.web.reactive.function.client.WebClient
import reactor.core.publisher.Mono
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteBatchRequestDTO
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteDTO
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteSlotAngleDTO
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteSlotProfileDTO
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteSummaryDTO
import space.nstart.pcp.slots_service.configuration.CustomErrorException
import space.nstart.pcp.slots_service.configuration.CustomValidationException
import space.nstart.pcp.slots_service.model.satellite.AbstractSatellite
import space.nstart.pcp.slots_service.model.satellite.TestSatelliteImpl

interface SatelliteCatalogClient {
    fun allSatellites(): List<AbstractSatellite>
    fun satellites(ids: List<Long>): List<AbstractSatellite>
    fun satellite(id: Long): AbstractSatellite
    fun updateSlotProfile(id: Long, request: SatelliteSlotProfileDTO): SatelliteSlotProfileDTO
}

@Service
class SatelliteCatalogWebClient(
    webClientBuilderProvider: ObjectProvider<WebClient.Builder>
) : SatelliteCatalogClient {

    private val webClientBuilder: WebClient.Builder = webClientBuilderProvider.ifAvailable ?: WebClient.builder()
    private val objectMapper = ObjectMapper().findAndRegisterModules()

    @Value("\${settings.satellite-catalog-service:pcp-satellite-catalog-service}")
    private val satelliteCatalogServiceUrl = ""

    override fun allSatellites(): List<AbstractSatellite> {
        val summaries = webClientBuilder.build()
            .get()
            .uri("$satelliteCatalogServiceUrl/api/satellites")
            .retrieve()
            .onStatus(HttpStatusCode::isError, ::mapError)
            .bodyToFlux(SatelliteSummaryDTO::class.java)
            .collectList()
            .block()
            .orEmpty()
        if (summaries.isEmpty()) {
            return emptyList()
        }
        return fetchSatellites(summaries.map { it.id })
    }

    override fun satellites(ids: List<Long>): List<AbstractSatellite> {
        val uniqueIds = ids.distinct()
        if (uniqueIds.isEmpty()) {
            return emptyList()
        }

        val byId = fetchSatellites(uniqueIds).associateBy { it.id }
        return uniqueIds.map { satelliteId ->
            byId[satelliteId] ?: throw CustomValidationException("КА $satelliteId не зарегистрирован")
        }
    }

    override fun satellite(id: Long): AbstractSatellite =
        webClientBuilder.build()
            .get()
            .uri("$satelliteCatalogServiceUrl/api/satellites/$id")
            .retrieve()
            .onStatus(HttpStatusCode::isError, ::mapError)
            .bodyToMono(SatelliteDTO::class.java)
            .map { it.toAbstractSatellite() }
            .block()
            ?: throw CustomErrorException("Не удалось получить параметры КА $id: пустой ответ")

    override fun updateSlotProfile(id: Long, request: SatelliteSlotProfileDTO): SatelliteSlotProfileDTO =
        webClientBuilder.build()
            .put()
            .uri("$satelliteCatalogServiceUrl/api/satellites/$id/slot-profile")
            .bodyValue(request)
            .retrieve()
            .onStatus(HttpStatusCode::isError, ::mapError)
            .bodyToMono(SatelliteSlotProfileDTO::class.java)
            .block()
            ?: throw CustomErrorException("Не удалось обновить slot-profile КА $id: пустой ответ")

    private fun fetchSatellites(ids: List<Long>): List<AbstractSatellite> =
        webClientBuilder.build()
            .post()
            .uri("$satelliteCatalogServiceUrl/api/satellites/batch")
            .bodyValue(SatelliteBatchRequestDTO(ids = ids))
            .retrieve()
            .onStatus(HttpStatusCode::isError, ::mapError)
            .bodyToFlux(SatelliteDTO::class.java)
            .map { it.toAbstractSatellite() }
            .collectList()
            .block()
            .orEmpty()

    private fun SatelliteDTO.toAbstractSatellite(): AbstractSatellite {
        val observationProfileValue = observationProfile
            ?: throw CustomErrorException("Для КА $id в каталоге не заполнен observation-profile")
        val slotProfileValue = slotProfile
            ?: throw CustomErrorException("Для КА $id в каталоге не заполнен slot-profile")
        val tnCalcValue = slotProfileValue.tnCalc
            ?: throw CustomErrorException("Для КА $id в каталоге не заполнен slot-profile.tnCalc")

        return TestSatelliteImpl(
            id = id,
            cycleRevs = slotProfileValue.cycleRevs,
            tnCalc = tnCalcValue,
            durationCalc = slotProfileValue.durationCalcDays,
            slotDuration = slotProfileValue.slotDuration,
            maxChainLength = slotProfileValue.maxChainLength,
            maxSurveyDuration = observationProfileValue.durationMaxSeconds.toInt(),
            mmi = observationProfileValue.mmiSeconds,
            angles = slotProfileValue.defaultAngles.flattenAngles()
        )
    }

    private fun List<SatelliteSlotAngleDTO>.flattenAngles(): List<Double> =
        flatMap { listOf(it.angleBegin, it.angleEnd) }

    private fun mapError(response: ClientResponse): Mono<Throwable> =
        response.bodyToMono(String::class.java)
            .defaultIfEmpty(response.statusCode().toString())
            .flatMap { body ->
                val message = extractErrorMessage(body)
                val exception = if (response.statusCode().is4xxClientError) {
                    CustomValidationException(message)
                } else {
                    CustomErrorException(message)
                }
                Mono.error(exception)
            }

    private fun extractErrorMessage(body: String): String {
        val text = body.trim()
        if (text.isEmpty()) {
            return "Ошибка обращения к каталогу спутников"
        }

        return runCatching {
            val node = objectMapper.readTree(text)
            when {
                node.hasNonNull("error") -> node["error"].asText()
                node.hasNonNull("message") -> node["message"].asText()
                else -> text
            }
        }.getOrDefault(text)
    }
}
