package space.nstart.pcp.pcp_satellites_service.service

import org.locationtech.jts.io.WKTReader
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import space.nstart.pcp.pcp_satellites_service.configuration.CustomErrorException
import space.nstart.pcp.pcp_satellites_service.model.BLS
import space.nstart.pcp.pcp_satellites_service.model.LongTermMission
import space.nstart.pcp.pcp_satellites_service.model.SatelliteModel
import space.nstart.pcp.pcp_satellites_service.model.mode.SurveyMode
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteDTO
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.DailyDurationDTO
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.MissionStatisticsDTO
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.RevolutionDurationDTO
import java.time.Duration
import java.time.LocalDateTime
import kotlin.system.measureTimeMillis

@Service
class SatelliteService(
    private val satelliteCatalogClient: SatelliteCatalogClient
) {

    private val logger = LoggerFactory.getLogger(this::class.java)
    private val wktReader = WKTReader()
    private val satellitesById = linkedMapOf<Long, SatelliteModel>()

    val satellites: List<SatelliteModel>
        get() = ensureLoaded().values.toList()

    fun clear() {
        satellites.forEach { satellite -> satellite.longMission.surveys.clear() }
    }

    fun all() = satellites.map { sat -> sat.toDTO() }

    fun byId(id: Long) = ensureLoaded()[id]?.toDTO()

    fun complexMission(id: Long) =
        ensureLoaded()[id]
            ?.longMission?.surveys?.sortedBy { survey -> survey.time }?.map { survey -> survey.toDTO() }
            ?: throw CustomErrorException("Нет КА с заданным идентификаторм $id")

    fun missionStatistics(id: Long): MissionStatisticsDTO {
        val mission = ensureLoaded()[id]
            ?.longMission
            ?: throw CustomErrorException("Нет КА с заданным идентификаторм $id")

        return MissionStatisticsDTO(
            modesCount = mission.surveys.size,
            totalContourArea = mission.surveys.sumOf { survey -> contourArea(survey.contourWKT) },
            revolutionDurations = mission.surveys
                .groupBy { it.revolution }
                .toSortedMap()
                .map { (revolution, surveys) ->
                    RevolutionDurationDTO(
                        revolution = revolution,
                        totalDuration = surveys.sumOf { survey -> survey.duration }
                    )
                },
            dailyDurations = mission.surveys
                .flatMap { survey -> splitSurveyDurationByDay(survey) }
                .groupBy({ it.first }, { it.second })
                .toSortedMap()
                .map { (date, durations) ->
                    DailyDurationDTO(
                        date = date,
                        totalDuration = durations.sum()
                    )
                }
        )
    }

    fun complexMissionForPaint(id: Long): String {
        val plan = ensureLoaded()[id]
            ?.longMission?.surveys
            ?: throw CustomErrorException("Нет КА с заданным идентификаторм $id")

        var geom = "GEOMETRYCOLLECTION("
        plan.forEach { survey ->
            geom += "${survey.contourWKT},"
        }
        geom = geom.dropLast(1)
        geom += ")"

        return geom
    }

    fun hasResources(
        tStart: LocalDateTime,
        tStop: LocalDateTime,
        available: List<Long>,
        sats: MutableList<Long>
    ): Boolean {
        sats.clear()
        var currentDay = tStart.toLocalDate()
        val lastDay = tStop.toLocalDate().minusDays(1)
        var hasResource = false

        while (currentDay <= lastDay) {
            satellites.forEach { satellite ->
                if (available.contains(satellite.satelliteId) && satellite.hasResources(currentDay)) {
                    hasResource = true
                    if (!sats.contains(satellite.satelliteId)) {
                        sats.add(satellite.satelliteId)
                    }
                }
            }
            currentDay = currentDay.plusDays(1)
        }
        return hasResource
    }

    private fun ensureLoaded(): Map<Long, SatelliteModel> {
        if (satellitesById.isNotEmpty()) {
            logger.info("Satellite cache hit: satellites={}", satellitesById.size)
            return satellitesById
        }

        synchronized(satellitesById) {
            if (satellitesById.isEmpty()) {
                val durationMs = measureTimeMillis {
                    satelliteCatalogClient.allSatellites()
                        .sortedBy { it.id }
                        .forEach { satellite ->
                            satellitesById[satellite.id] = satellite.toSatelliteModel()
                        }
                }
                logger.info(
                    "Satellite cache loaded: satellites={}, durationMs={}",
                    satellitesById.size,
                    durationMs
                )
            }
        }
        return satellitesById
    }

    private fun SatelliteDTO.toSatelliteModel() = SatelliteModel(
        satelliteId = id,
        name = name,
        red = visualization.red,
        green = visualization.green,
        blue = visualization.blue,
        longMission = LongTermMission(),
        bls = observationProfile?.let {
            BLS(
                captureAngle = it.captureAngle,
                sunAngleMin = it.sunAngleMin,
                durationMin = it.durationMinSeconds.toDouble(),
                durationMax = it.durationMaxSeconds.toDouble(),
                mmi = it.mmiSeconds.toDouble(),
                dailyMaxDuration = it.dailyMaxDurationSeconds.toDouble(),
                revolutionMaxDuration = it.revolutionMaxDurationSeconds.toDouble()
            )
        } ?: BLS(),
        scanTLE = scanTle
    )

    private fun contourArea(contourWkt: String): Double =
        if (contourWkt.isBlank()) {
            0.0
        } else {
            runCatching { wktReader.read(contourWkt).area }.getOrDefault(0.0)
        }

    private fun splitSurveyDurationByDay(survey: SurveyMode): List<Pair<java.time.LocalDate, Double>> {
        if (!survey.timeStop.isAfter(survey.time)) {
            return listOf(survey.time.toLocalDate() to survey.duration)
        }

        val durations = mutableListOf<Pair<java.time.LocalDate, Double>>()
        var current = survey.time

        while (current.toLocalDate() < survey.timeStop.toLocalDate()) {
            val nextDayStart = current.toLocalDate().plusDays(1).atStartOfDay()
            durations += current.toLocalDate() to Duration.between(current, nextDayStart).toSeconds().toDouble()
            current = nextDayStart
        }

        durations += current.toLocalDate() to Duration.between(current, survey.timeStop).toSeconds().toDouble()
        return durations
    }
}
