package space.nstart.pcp.pcp_satellites_service.service

import org.slf4j.LoggerFactory
import org.springframework.beans.factory.ObjectProvider
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
import org.springframework.web.reactive.function.client.WebClient
import reactor.core.publisher.Mono
import space.nstart.pcp.pcp_satellites_service.configuration.CustomErrorException
import space.nstart.pcp.pcp_types_lib.dto.coverage.CoverageSchemeCalculateRequestDTO
import space.nstart.pcp.pcp_types_lib.dto.coverage.CoverageSchemeResponseDTO
import space.nstart.pcp.pcp_types_lib.dto.requests.CoverageCalculationSource
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.OccupiedIntervalDTO
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.SurveySlotDTO
import tools.jackson.databind.ObjectMapper
import java.time.LocalDateTime
import kotlin.system.measureTimeMillis

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

    override val source: CoverageCalculationSource = CoverageCalculationSource.COVERAGE_SCHEME

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

    companion object {
        private const val LOG_INTERVAL_SAMPLE_LIMIT = 10
    }

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

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

    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(
            "coverage-scheme-service coverage start: targets={}, satellites={}, occupiedIntervals={}, interval=[{} - {}]",
            targets.size,
            selectedSatelliteIds.size,
            occupiedIntervals.size,
            intervalStart,
            intervalEnd
        )
        if (occupiedIntervals.isNotEmpty()) {
            logger.warn(
                "coverage-scheme-service client received occupiedIntervals but does not pass them to coverage-scheme-service request: occupiedIntervals={}, bySource={}, sample={}",
                occupiedIntervals.size,
                occupiedIntervals.groupingBy { it.source ?: "UNKNOWN" }.eachCount(),
                occupiedIntervals.take(LOG_INTERVAL_SAMPLE_LIMIT).map { interval ->
                    "satelliteId=${interval.satelliteId},start=${interval.startTime},end=${interval.endTime},roll=${interval.roll},source=${interval.source}"
                }
            )
        }

        val result = linkedMapOf<Long, List<SlotDTO>>()
        val durationMs = measureTimeMillis {
            targets.forEach { target ->
                lateinit var response: CoverageSchemeResponseDTO
                val targetDurationMs = measureTimeMillis {
                    response = calculateTarget(
                        target = target,
                        intervalStart = intervalStart,
                        intervalEnd = intervalEnd,
                        satelliteIds = selectedSatelliteIds,
                        sun = sun
                    )
                }
                result[target.targetId] = response.coverageScheme.map { it.toSlotDTO() }
                logger.info(
                    "coverage-scheme-service target calculated: targetId={}, candidateSlots={}, durationMs={}",
                    target.targetId,
                    response.coverageScheme.size,
                    targetDurationMs
                )
            }
        }

        logger.info(
            "coverage-scheme-service coverage finish: targets={}, returnedTargets={}, candidateSlots={}, durationMs={}",
            targets.size,
            result.size,
            result.values.sumOf { it.size },
            durationMs
        )

        return result
    }

    private fun calculateTarget(
        target: SlotCoverageTargetDTO,
        intervalStart: LocalDateTime,
        intervalEnd: LocalDateTime,
        satelliteIds: List<Long>,
        sun: Double?
    ): CoverageSchemeResponseDTO =
        client()
            .post()
            .uri("/api/coverage-schemes/calculate")
            .bodyValue(
                CoverageSchemeCalculateRequestDTO(
                    polygonWkt = target.wkt,
                    satelliteIds = satelliteIds,
                    timeStart = intervalStart,
                    timeEnd = intervalEnd,
                    sunAngle = sun
                )
            )
            .retrieve()
            .onStatus({ status -> status.isError }) { response ->
                response.bodyToMono(String::class.java)
                    .defaultIfEmpty("error")
                    .flatMap { body -> Mono.error(CustomErrorException(extractErrorMessage(body))) }
            }
            .bodyToMono(CoverageSchemeResponseDTO::class.java)
            .block()
            ?: CoverageSchemeResponseDTO()

    private fun SurveySlotDTO.toSlotDTO() =
        SlotDTO(
            satelliteId = satelliteId,
            tn = tn,
            tk = tk,
            roll = roll,
            contour = contour,
            revolution = revolution,
            revolutionSign = revolutionSign,
            latitude = latitude,
            longitude = longitude
        )

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