package space.nstart.pcp.slots_service.service

import org.springframework.beans.factory.ObjectProvider
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
import org.springframework.web.util.UriComponentsBuilder
import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.reactive.function.client.bodyToFlux
import space.nstart.pcp.pcp_types_lib.dto.ballistics.DurationOnRevDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.InitialConditionsDTO
import reactor.core.publisher.Mono
import space.nstart.pcp.pcp_types_lib.dto.ballistics.IntervalDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RadioVisibilityAreaDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteICDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.StationDTO
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.BookedSlotDTO
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.BookingRequestDTO
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCalculationSummaryDTO
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotDTO
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SurveySlotDTO
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteSlotProfileDTO
import tools.jackson.databind.ObjectMapper
import space.nstart.pcp.slots_service.configuration.CustomErrorException
import space.nstart.pcp.pcp_types_lib.dto.BookedSlotDetailsDTO
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.time.LocalDateTime

@Service
class SlotService(webClientBuilderProvider: ObjectProvider<WebClient.Builder>) {
    private val webClientBuilder: WebClient.Builder = webClientBuilderProvider.ifAvailable ?: WebClient.builder()

    @Value("\${settings.slots-service:slots-service}")
    val url = ""

    fun slots(
        id : String,
        tn : LocalDateTime,
        tk : LocalDateTime,
        sign : RevolutionSign?,
        cov : Boolean?,
        satellites : List<Long>? = null,
        sun : Double? = null
    ) =
        webClientBuilder.build()
            .get()
            .uri(
                UriComponentsBuilder
                    .fromUriString("$url/api/slots/request-cover")
                    .queryParam("requestId", id)
                    .queryParam("timeStart", UtcDateTimes.format(tn))
                    .queryParam("timeStop", UtcDateTimes.format(tk))
                    .queryParamIfPresent("revSign", java.util.Optional.ofNullable(sign))
                    .queryParam("cov", cov)
                    .queryParamIfPresent("sun", java.util.Optional.ofNullable(sun))
                    .apply {
                        satellites?.forEach { queryParam("satellites", it) }
                    }
                    .build()
                    .toUriString()
            )
            .retrieve()
            .onStatus({ status -> status.isError }) { response ->
                response.bodyToMono(String::class.java)
                    .defaultIfEmpty("error")
                    .flatMap { body -> Mono.error(CustomErrorException(extractErrorMessage(body))) }
            }
            .bodyToFlux<SlotDTO>()


    fun plan(id : Long, tn : LocalDateTime, tk : LocalDateTime) =
        webClientBuilder.build()
            .get()
            .uri("$url/api/slots/mission/satellite?satellite_id=$id&timeStart=${UtcDateTimes.format(tn)}&timeStop=${UtcDateTimes.format(tk)}")
            .retrieve()
            .onStatus({ status -> status.isError }) { response ->
                response.bodyToMono(String::class.java)
                    .defaultIfEmpty("error")
                    .flatMap { body -> Mono.error(CustomErrorException(extractErrorMessage(body))) }
            }
            .bodyToFlux< SurveySlotDTO>()

    fun allSlotsByInterval(id: Long, tn: LocalDateTime, tk: LocalDateTime): List<SlotDTO> =
        webClientBuilder.build()
            .get()
            .uri("$url/api/slots/interval?satelliteId=$id&timeStart=${UtcDateTimes.format(tn)}&timeStop=${UtcDateTimes.format(tk)}")
            .retrieve()
            .onStatus({ status -> status.isError }) { response ->
                response.bodyToMono(String::class.java)
                    .defaultIfEmpty("error")
                    .flatMap { body -> Mono.error(CustomErrorException(extractErrorMessage(body))) }
            }
            .bodyToFlux(SlotDTO::class.java)
            .collectList()
            .block() ?: emptyList()

    fun slotCalculationSummaries(satelliteIds: List<Long>): List<SlotCalculationSummaryDTO> {
        if (satelliteIds.isEmpty()) {
            return emptyList()
        }

        return webClientBuilder.build()
            .get()
            .uri(
                UriComponentsBuilder
                    .fromUriString("$url/api/slots/calculation-summary")
                    .apply {
                        satelliteIds.distinct().forEach { queryParam("satelliteIds", it) }
                    }
                    .build()
                    .toUriString()
            )
            .retrieve()
            .onStatus({ status -> status.isError }) { response ->
                response.bodyToMono(String::class.java)
                    .defaultIfEmpty("error")
                    .flatMap { body -> Mono.error(CustomErrorException(extractErrorMessage(body))) }
            }
            .bodyToFlux(SlotCalculationSummaryDTO::class.java)
            .collectList()
            .block() ?: emptyList()
    }

    fun slotCalculationSummary(satelliteId: Long): SlotCalculationSummaryDTO =
        slotCalculationSummaries(listOf(satelliteId)).firstOrNull()
            ?: SlotCalculationSummaryDTO(satelliteId = satelliteId)

    fun startSlotCalculation(satelliteId: Long, slotProfile: SatelliteSlotProfileDTO) {
        webClientBuilder.build()
            .post()
            .uri(
                UriComponentsBuilder
                    .fromUriString("$url/api/slots/test-init")
                    .queryParam("sats", satelliteId)
                    .queryParam("slotDuration", slotProfile.slotDuration)
                    .queryParam("dailyDuration", slotProfile.durationCalcDays)
                    .queryParam("revolutionDuration", slotProfile.cycleRevs)
                    .queryParam("recover", false)
                    .build()
                    .toUriString()
            )
            .retrieve()
            .onStatus({ status -> status.isError }) { response ->
                response.bodyToMono(String::class.java)
                    .defaultIfEmpty("error")
                    .flatMap { body -> Mono.error(CustomErrorException(extractErrorMessage(body))) }
            }
            .toBodilessEntity()
            .block()
    }



    fun allBooked(): List<BookedSlotDTO> =
        webClientBuilder.build()
            .get()
            .uri("$url/api/slots/booking")
            .retrieve()
            .onStatus({ status -> status.isError }) { response ->
                response.bodyToMono(String::class.java)
                    .defaultIfEmpty("error")
                    .flatMap { body -> Mono.error(CustomErrorException(extractErrorMessage(body))) }
            }
            .bodyToFlux(BookedSlotDTO::class.java)
            .collectList()
            .block()
            ?: emptyList()

    fun bookedByIds(bookedSlotIds: List<Long>): List<BookedSlotDetailsDTO> =
        bookedDetailsByIds(bookedSlotIds)

    fun bookedDetailsByIds(bookedSlotIds: List<Long>): List<BookedSlotDetailsDTO> {
        val ids = bookedSlotIds.filter { it > 0 }.distinct().toSet()
        if (ids.isEmpty()) {
            return emptyList()
        }

        return searchBooked(
            timeStart = null,
            timeStop = null,
            requestId = null,
            satelliteIds = null
        ).filter { it.bookedSlotId in ids }
    }


    fun searchBooked(
        timeStart: LocalDateTime?,
        timeStop: LocalDateTime?,
        requestId: String?,
        satelliteIds: List<Long>?
    ): List<BookedSlotDetailsDTO> = webClientBuilder.build()
        .get()
        .uri(
            UriComponentsBuilder
                .fromUriString("$url/api/slots/booking/search")
                .queryParamIfPresent("timeStart", java.util.Optional.ofNullable(timeStart?.let { UtcDateTimes.format(it) }))
                .queryParamIfPresent("timeStop", java.util.Optional.ofNullable(timeStop?.let { UtcDateTimes.format(it) }))
                .queryParamIfPresent("requestId", java.util.Optional.ofNullable(requestId?.trim()?.takeIf { it.isNotEmpty() }))
                .apply {
                    satelliteIds.orEmpty().distinct().forEach { queryParam("satelliteIds", it) }
                }
                .build()
                .toUriString()
        )
        .retrieve()
        .onStatus({ status -> status.isError }) { response ->
            response.bodyToMono(String::class.java)
                .defaultIfEmpty("error")
                .flatMap { body -> Mono.error(CustomErrorException(extractErrorMessage(body))) }
        }
        .bodyToFlux(BookedSlotDetailsDTO::class.java)
        .collectList()
        .block()
        ?: emptyList()

    fun bookedByRequest(requestId: String): List<SlotDTO> =
        webClientBuilder.build()
            .get()
            .uri("$url/api/slots/booking/by-request/$requestId")
            .retrieve()
            .onStatus({ status -> status.isError }) { response ->
                response.bodyToMono(String::class.java)
                    .defaultIfEmpty("error")
                    .flatMap { body -> Mono.error(CustomErrorException(extractErrorMessage(body))) }
            }
            .bodyToFlux(SlotDTO::class.java)
            .collectList()
            .block()
            ?: emptyList()

    fun bookReq(req: BookingRequestDTO): List<BookedSlotDTO> =
        webClientBuilder.build()
            .post()
            .uri("$url/api/slots/booking/request")
            .bodyValue(req)
            .retrieve()
            .bodyToFlux<BookedSlotDTO>()
            .collectList()
            .block() ?: emptyList()

    fun cancelReq(requestId: String): Int =
        webClientBuilder.build()
            .delete()
            .uri("$url/api/slots/booking/request?requestId=$requestId")
            .retrieve()
            .onStatus({ status -> status.isError }) { response ->
                response.bodyToMono(String::class.java)
                    .defaultIfEmpty("error")
                    .flatMap { body -> Mono.error(CustomErrorException(extractErrorMessage(body))) }
            }
            .bodyToMono(Int::class.java)
            .block() ?: 0

    fun rvaCommon(satelliteId: Long, duration: Long, stations: List<StationDTO>): List<RadioVisibilityAreaDTO> =
        webClientBuilder.build()
            .post()
            .uri("$url/api/satellite/rva-common/$satelliteId/$duration")
            .bodyValue(stations)
            .retrieve()
            .onStatus({ status -> status.isError }) { response ->
                response.bodyToMono(String::class.java)
                    .defaultIfEmpty("error")
                    .flatMap { body -> Mono.error(CustomErrorException(extractErrorMessage(body))) }
            }
            .bodyToFlux(RadioVisibilityAreaDTO::class.java)
            .collectList()
            .block()
            ?: emptyList()

    fun rvaMerge(satelliteId: Long, duration: Long, stations: List<StationDTO>): List<IntervalDTO> =
        webClientBuilder.build()
            .post()
            .uri("$url/api/satellite/rva-merge/$satelliteId/$duration")
            .bodyValue(stations)
            .retrieve()
            .onStatus({ status -> status.isError }) { response ->
                response.bodyToMono(String::class.java)
                    .defaultIfEmpty("error")
                    .flatMap { body -> Mono.error(CustomErrorException(extractErrorMessage(body))) }
            }
            .bodyToFlux(IntervalDTO::class.java)
            .collectList()
            .block()
            ?: emptyList()

    fun rvaMergeOnRev(satelliteId: Long, duration: Long, stations: List<StationDTO>): List<DurationOnRevDTO> =
        webClientBuilder.build()
            .post()
            .uri("$url/api/satellite/rva-merge-on-rev/$satelliteId/$duration")
            .bodyValue(stations)
            .retrieve()
            .onStatus({ status -> status.isError }) { response ->
                response.bodyToMono(String::class.java)
                    .defaultIfEmpty("error")
                    .flatMap { body -> Mono.error(CustomErrorException(extractErrorMessage(body))) }
            }
            .bodyToFlux(DurationOnRevDTO::class.java)
            .collectList()
            .block()
            ?: emptyList()

    fun satelliteInitialConditions(satelliteId: Long): SatelliteICDTO? =
        webClientBuilder.build()
            .get()
            .uri("$url/api/satellite-ic/$satelliteId")
            .exchangeToMono { response ->
                when {
                    response.statusCode().is2xxSuccessful -> response.bodyToMono(SatelliteICDTO::class.java)
                    response.statusCode().is4xxClientError -> response.bodyToMono(String::class.java)
                        .defaultIfEmpty("error")
                        .flatMap { body ->
                            val message = extractErrorMessage(body)
                            if (message.contains("не зарегистрированы")) {
                                Mono.empty()
                            } else {
                                Mono.error(CustomErrorException(message))
                            }
                        }

                    else -> response.bodyToMono(String::class.java)
                        .defaultIfEmpty("error")
                        .flatMap { body -> Mono.error(CustomErrorException(extractErrorMessage(body))) }
                }
            }
            .block()


    fun sendSatelliteInitialConditions(satelliteId: Long, time: LocalDateTime) {
        webClientBuilder.build()
            .post()
            .uri(
                UriComponentsBuilder
                    .fromUriString("$url/api/satellite/{satelliteId}/send-ic")
                    .queryParam("time", UtcDateTimes.format(time))
                    .buildAndExpand(satelliteId)
                    .toUriString()
            )
            .retrieve()
            .onStatus({ status -> status.isError }) { response ->
                response.bodyToMono(String::class.java)
                    .defaultIfEmpty("error")
                    .flatMap { body -> Mono.error(CustomErrorException(extractErrorMessage(body))) }
            }
            .toBodilessEntity()
            .block()
    }

    fun saveSatelliteInitialConditions(
        satelliteId: Long,
        request: InitialConditionsDTO,
        create: Boolean
    ): SatelliteICDTO {
        val methodSpec = if (create) webClientBuilder.build().post() else webClientBuilder.build().put()
        return methodSpec
            .uri(
                if (create) "$url/api/satellite-ic"
                else "$url/api/satellite-ic/$satelliteId"
            )
            .bodyValue(
                if (create) SatelliteICDTO(satelliteId = satelliteId, ic = request)
                else request
            )
            .retrieve()
            .onStatus({ status -> status.isError }) { response ->
                response.bodyToMono(String::class.java)
                    .defaultIfEmpty("error")
                    .flatMap { body -> Mono.error(CustomErrorException(extractErrorMessage(body))) }
            }
            .bodyToMono(SatelliteICDTO::class.java)
            .block() ?: throw CustomErrorException("Не удалось сохранить начальные условия для КА $satelliteId")
    }

    private fun extractErrorMessage(body: String): String {
        val text = body.trim()
        if (text.isEmpty()) return "error"

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