package space.nstart.pcp.pcp_satellites_service.service

import org.springframework.beans.factory.ObjectProvider
import org.springframework.beans.factory.annotation.Value
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import org.springframework.web.reactive.function.client.WebClientRequestException
import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.reactive.function.client.bodyToFlux
import reactor.core.publisher.Mono
import space.nstart.pcp.pcp_satellites_service.configuration.CustomErrorException
import space.nstart.pcp.pcp_types_lib.dto.requests.CoverageCalculationSource
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SatelliteMissionBatchRequestDTO
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SatelliteMissionBatchResponseDTO
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.OccupiedIntervalDTO
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCoverageBatchRequestDTO
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCoverageBatchResponseDTO
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCoverageStrategy
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCoverageTargetDTO
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotDTO
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotStatus
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SurveySlotDTO
import tools.jackson.databind.ObjectMapper
import java.time.LocalDateTime
import kotlin.system.measureTimeMillis

@Service
class CoverageBatchClient(webClientBuilderProvider: ObjectProvider<WebClient.Builder>) : CoverageCalculationClient {

    override val source: CoverageCalculationSource = CoverageCalculationSource.SLOTS

    private val webClientBuilder: WebClient.Builder = webClientBuilderProvider.ifAvailable ?: WebClient.builder()
    private val logger = LoggerFactory.getLogger(this::class.java)

    private data class CoverageBatchCallResult(
        val response: List<SlotCoverageBatchResponseDTO>,
        val durationMs: Long,
        val coverageStrategy: SlotCoverageStrategy
    )

    private class CoverageBatchRemoteException(
        val statusCode: Int,
        message: String
    ) : RuntimeException(message)

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

    private fun client() = webClientBuilder.baseUrl(url).build()

    fun plannedModes(
        satelliteIds: List<Long>,
        intervalStart: LocalDateTime,
        intervalEnd: LocalDateTime
    ): Map<Long, List<SurveySlotDTO>> {
        if (satelliteIds.isEmpty()) {
            return emptyMap()
        }

        val selectedSatelliteIds = satelliteIds.distinct()
        logger.info(
            "slots-service plannedModes batch start: satellites={}, interval=[{} - {}]",
            selectedSatelliteIds.size,
            intervalStart,
            intervalEnd
        )

        var response = emptyList<SatelliteMissionBatchResponseDTO>()
        val durationMs = measureTimeMillis {
            response = client()
                .post()
                .uri("/api/slots/mission/satellites")
                .bodyValue(
                    SatelliteMissionBatchRequestDTO(
                        satelliteIds = selectedSatelliteIds,
                        timeStart = intervalStart,
                        timeStop = intervalEnd
                    )
                )
                .retrieve()
                .onStatus({ status -> status.isError }) { response ->
                    response.bodyToMono(String::class.java)
                        .defaultIfEmpty("error")
                        .flatMap { body -> Mono.error(CustomErrorException(extractErrorMessage(body))) }
                }
                .bodyToFlux<SatelliteMissionBatchResponseDTO>()
                .collectList()
                .block()
                .orEmpty()
        }

        logger.info(
            "slots-service plannedModes batch finish: satellites={}, chains={}, durationMs={}",
            selectedSatelliteIds.size,
            response.sumOf { it.slots.size },
            durationMs
        )

        return response.associate { it.satelliteId to it.slots }
    }

    override fun coverageModes(
        targets: List<SlotCoverageTargetDTO>,
        intervalStart: LocalDateTime,
        intervalEnd: LocalDateTime,
        satelliteIds: List<Long>,
        occupiedIntervals: List<OccupiedIntervalDTO>,
        sun: Double?,
        coverageStrategy: SlotCoverageStrategy?
    ): Map<Long, List<SlotDTO>> {
        if (targets.isEmpty() || satelliteIds.isEmpty()) {
            return emptyMap()
        }

        val selectedSatelliteIds = satelliteIds.distinct()
        logger.info(
            "slots-service coverage batch start: targets={}, satellites={}, occupiedIntervals={}, interval=[{} - {}], sun={}, coverageStrategy={}",
            targets.size,
            selectedSatelliteIds.size,
            occupiedIntervals.size,
            intervalStart,
            intervalEnd,
            sun,
            coverageStrategy
        )

        val effectiveStrategy = coverageStrategy ?: SlotCoverageStrategy.GREEDY

        val result = try {
            requestCoverageModes(
                targets = targets,
                intervalStart = intervalStart,
                intervalEnd = intervalEnd,
                selectedSatelliteIds = selectedSatelliteIds,
                occupiedIntervals = occupiedIntervals,
                sun = sun,
                coverageStrategy = effectiveStrategy
            )
        } catch (ex: Exception) {
            if (effectiveStrategy == SlotCoverageStrategy.CONTINUOUS && isContinuousFallbackCandidate(ex)) {
                logger.warn(
                    "slots-service coverage batch CONTINUOUS failed, fallback to GREEDY: targets={}, satellites={}, occupiedIntervals={}, interval=[{} - {}], causeType={}, cause={}",
                    targets.size,
                    selectedSatelliteIds.size,
                    occupiedIntervals.size,
                    intervalStart,
                    intervalEnd,
                    ex::class.simpleName,
                    ex.message
                )
                requestCoverageModes(
                    targets = targets,
                    intervalStart = intervalStart,
                    intervalEnd = intervalEnd,
                    selectedSatelliteIds = selectedSatelliteIds,
                    occupiedIntervals = occupiedIntervals,
                    sun = sun,
                    coverageStrategy = SlotCoverageStrategy.GREEDY
                )
            } else {
                throw toCustomError(ex)
            }
        }

        val rawCandidateSlots = result.response.sumOf { it.slots.size }
        val filteredResponse = removeBookedSlotsFromCoverageResponse(result.response)
        val filteredCandidateSlots = filteredResponse.sumOf { it.slots.size }
        val removedBookedSlots = rawCandidateSlots - filteredCandidateSlots

        logger.info(
            "slots-service coverage batch finish: targets={}, returnedTargets={}, candidateSlotsRaw={}, candidateSlotsAfterBookedFilter={}, removedBookedSlots={}, durationMs={}, effectiveCoverageStrategy={}",
            targets.size,
            filteredResponse.size,
            rawCandidateSlots,
            filteredCandidateSlots,
            removedBookedSlots,
            result.durationMs,
            result.coverageStrategy
        )

        return filteredResponse.associate { it.targetId to it.slots }
    }


    /**
     * Complex plan loads booked slots before asking slots-service for coverage candidates.
     * slots-service still has to see booked slots internally: it marks them as BOOKED and the
     * coverage solver subtracts their geometry from the uncovered target area. However these
     * BOOKED slots are already present in the working complex plan and must not be returned
     * to chunk processing again as COMPLAN candidates.
     */
    private fun removeBookedSlotsFromCoverageResponse(
        response: List<SlotCoverageBatchResponseDTO>
    ): List<SlotCoverageBatchResponseDTO> =
        response.map { targetResponse ->
            targetResponse.copy(
                slots = targetResponse.slots.filterNot { slot -> slot.state != SlotStatus.AVAILABLE }
            )
        }

    private fun requestCoverageModes(
        targets: List<SlotCoverageTargetDTO>,
        intervalStart: LocalDateTime,
        intervalEnd: LocalDateTime,
        selectedSatelliteIds: List<Long>,
        occupiedIntervals: List<OccupiedIntervalDTO>,
        sun: Double?,
        coverageStrategy: SlotCoverageStrategy
    ): CoverageBatchCallResult {
        var response = emptyList<SlotCoverageBatchResponseDTO>()
        val durationMs = measureTimeMillis {
            response = client()
                .post()
                .uri("/api/slots/poly-cover/batch")
                .bodyValue(
                    SlotCoverageBatchRequestDTO(
                        items = targets,
                        timeStart = intervalStart,
                        timeStop = intervalEnd,
                        cov = true,
                        satellites = selectedSatelliteIds,
                        occupiedIntervals = occupiedIntervals,
                        sun = sun,
                        coverageStrategy = coverageStrategy
                    )
                )
                .retrieve()
                .onStatus({ status -> status.isError }) { response ->
                    val statusCode = response.statusCode().value()
                    response.bodyToMono(String::class.java)
                        .defaultIfEmpty("error")
                        .flatMap { body ->
                            Mono.error(CoverageBatchRemoteException(statusCode, extractErrorMessage(body)))
                        }
                }
                .bodyToFlux<SlotCoverageBatchResponseDTO>()
                .collectList()
                .block()
                .orEmpty()
        }
        return CoverageBatchCallResult(
            response = response,
            durationMs = durationMs,
            coverageStrategy = coverageStrategy
        )
    }

    private fun isContinuousFallbackCandidate(ex: Exception): Boolean =
        when (ex) {
            is CoverageBatchRemoteException -> ex.statusCode >= 500
            is WebClientRequestException -> true
            else -> false
        }

    private fun toCustomError(ex: Exception): CustomErrorException =
        when (ex) {
            is CustomErrorException -> ex
            is CoverageBatchRemoteException -> CustomErrorException(ex.message ?: "error")
            else -> CustomErrorException(ex.message ?: "error")
        }

    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)
    }
}
