package space.nstart.pcp.pcp_satellites_service.service

import org.locationtech.jts.io.WKTReader
import org.slf4j.LoggerFactory
import org.springframework.data.domain.Sort
import org.springframework.data.jpa.domain.Specification
import org.springframework.stereotype.Service
import space.nstart.pcp.pcp_satellites_service.configuration.CustomValidationException
import space.nstart.pcp.pcp_satellites_service.entity.SatelliteModeEntity
import space.nstart.pcp.pcp_satellites_service.model.mode.SurveyMode
import space.nstart.pcp.pcp_satellites_service.repository.SatelliteModeBookedSlotRepository
import space.nstart.pcp.pcp_satellites_service.repository.SatelliteModeCellRepository
import space.nstart.pcp.pcp_satellites_service.repository.SatelliteModeRepository
import space.nstart.pcp.pcp_satellites_service.repository.SatelliteModeSnapshotRepository
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 space.nstart.pcp.pcp_types_lib.dto.satellite.mission.SatelliteModeResponseDTO
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.SatelliteModeSnapshotResponseDTO
import space.nstart.pcp.pcp_types_lib.dto.satellite.modes.SurveyModeInfoDTO
import java.time.Duration
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
import kotlin.system.measureTimeMillis

@Service
class SatelliteModeQueryService(
    private val satelliteModeRepository: SatelliteModeRepository,
    private val satelliteModeSnapshotRepository: SatelliteModeSnapshotRepository,
    private val satelliteModeBookedSlotRepository: SatelliteModeBookedSlotRepository,
    private val satelliteModeCellRepository: SatelliteModeCellRepository,
    private val satelliteModeMapper: SatelliteModeMapper,
    private val satelliteModeSnapshotMapper: SatelliteModeSnapshotMapper
) {

    private data class ModeKey(
        val id: Long?,
        val satelliteId: Long,
        val startTime: LocalDateTime,
        val endTime: LocalDateTime
    )

    private val wktReader = WKTReader()
    private val logger = LoggerFactory.getLogger(this::class.java)

    fun findMissionModes(satelliteId: Long): List<SurveyModeInfoDTO> =
        satelliteModeRepository.findAllBySnapshot_SatelliteIdAndSnapshot_IsActiveTrueOrderByStartTimeAsc(satelliteId)
            .let { modes ->
                val cellNumsByModeId = cellNumsByModeId(modes.mapNotNull { it.id })
                modes.map { entity ->
                    satelliteModeMapper.toSurveyMode(
                        entity = entity,
                        cellNums = cellNumsByModeId[entity.id].orEmpty()
                    ).toDTO()
                }
            }

    fun missionStatistics(satelliteId: Long): MissionStatisticsDTO {
        val surveys = satelliteModeRepository.findAllBySnapshot_SatelliteIdAndSnapshot_IsActiveTrueOrderByStartTimeAsc(satelliteId)
            .let { modes ->
                val cellNumsByModeId = cellNumsByModeId(modes.mapNotNull { it.id })
                modes.map { entity ->
                    satelliteModeMapper.toSurveyMode(
                        entity = entity,
                        cellNums = cellNumsByModeId[entity.id].orEmpty()
                    )
                }
            }

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

    fun buildPaintGeometry(satelliteId: Long): String {
        val contours = satelliteModeRepository.findAllBySnapshot_SatelliteIdAndSnapshot_IsActiveTrueOrderByStartTimeAsc(satelliteId)
            .mapNotNull { it.contourWkt?.takeIf(String::isNotBlank) }

        if (contours.isEmpty()) {
            return "GEOMETRYCOLLECTION EMPTY"
        }

        return contours.joinToString(
            prefix = "GEOMETRYCOLLECTION(",
            postfix = ")"
        )
    }

    fun buildCellPaintGeometry(cellNum: Long): String {
        val contours = satelliteModeRepository.findAllByRelatedCellNumOrderByStartTimeAsc(cellNum)
            .mapNotNull { it.contourWkt?.takeIf(String::isNotBlank) }

        if (contours.isEmpty()) {
            return "GEOMETRYCOLLECTION EMPTY"
        }

        return contours.joinToString(
            prefix = "GEOMETRYCOLLECTION(",
            postfix = ")"
        )
    }

    fun findModesByInterval(
        satelliteIds: Collection<Long>,
        intervalStart: LocalDateTime,
        intervalEnd: LocalDateTime,
        runId: Long? = null
    ): List<SatelliteModeResponseDTO> {
        validateInterval(intervalStart, intervalEnd)
        if (satelliteIds.isEmpty()) {
            return emptyList()
        }

        val distinctSatelliteIds = satelliteIds.distinct()
        val snapshots = if (runId == null) {
            satelliteModeSnapshotRepository
                .findAllBySatelliteIdInAndIsActiveTrueOrderBySatelliteIdAscCreatedAtDesc(distinctSatelliteIds)
        } else {
            satelliteModeSnapshotRepository
                .findAllByComplexPlanRun_IdAndSatelliteIdInOrderBySatelliteIdAscCreatedAtDesc(
                    runId,
                    distinctSatelliteIds
                )
        }
        val snapshotIds = snapshots.distinctBy { it.satelliteId }.mapNotNull { it.id }

        return findModesBySnapshotIds(snapshotIds)
            .filter { mode -> mode.startTime < intervalEnd && intervalStart < mode.endTime }
    }

    fun findModesByRun(runId: Long): List<SatelliteModeResponseDTO> {
        val modes = satelliteModeRepository.findAllByComplexPlanRun_IdOrderByStartTimeAsc(runId)
        val bookedSlotsByModeId = bookedSlotsByModeId(modes.mapNotNull { it.id })
        val cellNumsByModeId = cellNumsByModeId(modes.mapNotNull { it.id })

        return modes.map { mode ->
            satelliteModeMapper.toResponse(
                entity = mode,
                bookedSlotIds = bookedSlotsByModeId[mode.id].orEmpty(),
                cellNums = cellNumsByModeId[mode.id].orEmpty()
            )
        }
    }

    fun findSnapshot(snapshotId: Long): SatelliteModeSnapshotResponseDTO =
        satelliteModeSnapshotMapper.toResponse(
            satelliteModeSnapshotRepository.findById(snapshotId)
                .orElseThrow { CustomValidationException("Satellite mode snapshot $snapshotId not found") }
        )

    fun findSnapshots(
        satelliteId: Long?,
        intervalStart: LocalDateTime?,
        intervalEnd: LocalDateTime?,
        activeOnly: Boolean
    ): List<SatelliteModeSnapshotResponseDTO> =
        satelliteModeSnapshotRepository.findAll(
            buildSnapshotSpecification(
                satelliteId = satelliteId,
                intervalStart = intervalStart,
                intervalEnd = intervalEnd,
                activeOnly = activeOnly
            ),
            Sort.by(
                Sort.Order.asc("satelliteId"),
                Sort.Order.desc("intervalStart"),
                Sort.Order.desc("createdAt"),
                Sort.Order.desc("id")
            )
        ).map(satelliteModeSnapshotMapper::toResponse)

    fun findActiveSnapshot(
        satelliteId: Long,
        intervalStart: LocalDateTime? = null,
        intervalEnd: LocalDateTime? = null
    ): SatelliteModeSnapshotResponseDTO? {
        if ((intervalStart == null) != (intervalEnd == null)) {
            throw CustomValidationException("Параметры intervalStart и intervalEnd должны передаваться вместе")
        }
        if (intervalStart != null && intervalEnd != null) {
            validateInterval(intervalStart, intervalEnd)
        }
        return satelliteModeSnapshotRepository
            .findFirstBySatelliteIdAndIsActiveTrueOrderByCreatedAtDescIdDesc(satelliteId)
            ?.let(satelliteModeSnapshotMapper::toResponse)
    }

    fun findModesBySnapshot(
        snapshotId: Long,
        intervalStart: LocalDateTime? = null,
        intervalEnd: LocalDateTime? = null
    ): List<SatelliteModeResponseDTO> {
        if ((intervalStart == null) != (intervalEnd == null)) {
            throw CustomValidationException("Параметры intervalStart и intervalEnd должны передаваться вместе")
        }
        if (intervalStart != null && intervalEnd != null) {
            validateInterval(intervalStart, intervalEnd)
        }
        return findModesBySnapshotIds(listOf(snapshotId))
            .filter { mode ->
                if (intervalStart == null || intervalEnd == null) {
                    true
                } else {
                    mode.startTime < intervalEnd && intervalStart < mode.endTime
                }
            }
    }

    fun loadWorkingModes(
        satelliteIds: Collection<Long>,
        intervalStart: LocalDateTime,
        intervalEnd: LocalDateTime
    ): Map<Long, MutableList<SurveyMode>> =
        loadConstraintModes(satelliteIds, intervalStart, intervalEnd)

    fun loadConstraintModes(
        satelliteIds: Collection<Long>,
        intervalStart: LocalDateTime,
        intervalEnd: LocalDateTime
    ): Map<Long, MutableList<SurveyMode>> {
        validateInterval(intervalStart, intervalEnd)
        if (satelliteIds.isEmpty()) {
            return emptyMap()
        }

        val distinctSatelliteIds = satelliteIds.distinct()
        lateinit var dayModes: List<SatelliteModeEntity>
        val dayModesMs = measureTimeMillis {
            dayModes = loadDayConstraintModeEntities(distinctSatelliteIds, intervalStart, intervalEnd)
        }
        logger.info(
            "Satellite mode query stage day-constraints loaded: satellites={}, modes={}, durationMs={}",
            distinctSatelliteIds,
            dayModes.size,
            dayModesMs
        )

        lateinit var revolutionModes: List<SatelliteModeEntity>
        val revolutionModesMs = measureTimeMillis {
            revolutionModes = loadRevolutionConstraintModeEntities(dayModes)
        }
        logger.info(
            "Satellite mode query stage revolution-constraints loaded: satellites={}, modes={}, durationMs={}",
            distinctSatelliteIds,
            revolutionModes.size,
            revolutionModesMs
        )

        val modes = (dayModes + revolutionModes)
            .distinctBy { entity -> ModeKey(entity.id, entity.satelliteId, entity.startTime, entity.endTime) }
            .sortedBy { it.startTime }

        lateinit var bookedSlotsByModeId: Map<Long, List<Long>>
        lateinit var cellNumsByModeId: Map<Long, List<Long>>
        val linksMs = measureTimeMillis {
            val modeIds = modes.mapNotNull { it.id }
            bookedSlotsByModeId = bookedSlotsByModeId(modeIds)
            cellNumsByModeId = cellNumsByModeId(modeIds)
        }
        logger.info(
            "Satellite mode query stage mode-links loaded: modes={}, bookedSlotLinks={}, cellLinks={}, durationMs={}",
            modes.size,
            bookedSlotsByModeId.values.sumOf { it.size },
            cellNumsByModeId.values.sumOf { it.size },
            linksMs
        )

        lateinit var result: Map<Long, MutableList<SurveyMode>>
        val mapMs = measureTimeMillis {
            result = modes
                .groupBy { it.satelliteId }
                .mapValues { (_, entities) ->
                    entities.map { entity ->
                        satelliteModeMapper.toSurveyMode(
                            entity = entity,
                            bookedSlotIds = bookedSlotsByModeId[entity.id].orEmpty(),
                            cellNums = cellNumsByModeId[entity.id].orEmpty()
                        )
                    }.toMutableList()
                }
        }
        logger.info(
            "Satellite mode query stage constraints mapped: satellites={}, modes={}, durationMs={}",
            result.keys,
            result.values.sumOf { it.size },
            mapMs
        )
        return result
    }

    fun loadDayConstraintModes(
        satelliteIds: Collection<Long>,
        intervalStart: LocalDateTime,
        intervalEnd: LocalDateTime
    ): Map<Long, MutableList<SurveyMode>> {
        lateinit var result: Map<Long, MutableList<SurveyMode>>
        val durationMs = measureTimeMillis {
            result = mapConstraintEntitiesToSurveys(loadDayConstraintModeEntities(satelliteIds, intervalStart, intervalEnd))
        }
        logger.info(
            "Satellite mode query day-constraint modes loaded: satellites={}, modes={}, durationMs={}",
            satelliteIds.distinct(),
            result.values.sumOf { it.size },
            durationMs
        )
        return result
    }

    fun loadRevolutionConstraintModes(
        satelliteIds: Collection<Long>,
        intervalStart: LocalDateTime,
        intervalEnd: LocalDateTime
    ): Map<Long, MutableList<SurveyMode>> {
        lateinit var result: Map<Long, MutableList<SurveyMode>>
        val durationMs = measureTimeMillis {
            val dayModes = loadDayConstraintModeEntities(satelliteIds, intervalStart, intervalEnd)
            result = mapConstraintEntitiesToSurveys(loadRevolutionConstraintModeEntities(dayModes))
        }
        logger.info(
            "Satellite mode query revolution-constraint modes loaded: satellites={}, modes={}, durationMs={}",
            satelliteIds.distinct(),
            result.values.sumOf { it.size },
            durationMs
        )
        return result
    }

    private fun loadDayConstraintModeEntities(
        satelliteIds: Collection<Long>,
        intervalStart: LocalDateTime,
        intervalEnd: LocalDateTime
    ): List<SatelliteModeEntity> {
        validateInterval(intervalStart, intervalEnd)
        if (satelliteIds.isEmpty()) {
            return emptyList()
        }

        val dayStart = intervalStart.toLocalDate().atStartOfDay()
        val lastTouchedDate = touchedDateRange(intervalStart, intervalEnd).last()
        val dayEndExclusive = lastTouchedDate.plusDays(1).atStartOfDay()
        return satelliteModeRepository
            .findAllBySnapshot_SatelliteIdInAndSnapshot_IsActiveTrueAndEndTimeGreaterThanAndStartTimeLessThanOrderByStartTimeAsc(
                satelliteIds.distinct(),
                dayStart,
                dayEndExclusive
            )
    }

    private fun loadRevolutionConstraintModeEntities(dayModes: List<SatelliteModeEntity>): List<SatelliteModeEntity> =
        dayModes
            .groupBy { it.satelliteId }
            .flatMap { (satelliteId, entities) ->
                val revolutions = entities.map { it.revolution }.toSet()
                if (revolutions.isEmpty()) {
                    emptyList()
                } else {
                    satelliteModeRepository.findAllBySnapshot_SatelliteIdAndSnapshot_IsActiveTrueAndRevolutionInOrderByStartTimeAsc(
                        satelliteId,
                        revolutions
                    )
                }
            }

    private fun mapConstraintEntitiesToSurveys(modes: List<SatelliteModeEntity>): Map<Long, MutableList<SurveyMode>> {
        if (modes.isEmpty()) {
            return emptyMap()
        }

        val uniqueModes = modes
            .distinctBy { entity -> ModeKey(entity.id, entity.satelliteId, entity.startTime, entity.endTime) }
            .sortedBy { it.startTime }
        val bookedSlotsByModeId = bookedSlotsByModeId(uniqueModes.mapNotNull { it.id })
        val cellNumsByModeId = cellNumsByModeId(uniqueModes.mapNotNull { it.id })

        return uniqueModes
            .groupBy { it.satelliteId }
            .mapValues { (_, entities) ->
                entities.map { entity ->
                    satelliteModeMapper.toSurveyMode(
                        entity = entity,
                        bookedSlotIds = bookedSlotsByModeId[entity.id].orEmpty(),
                        cellNums = cellNumsByModeId[entity.id].orEmpty()
                    )
                }.toMutableList()
            }
    }

    private fun findModesBySnapshotIds(snapshotIds: Collection<Long>): List<SatelliteModeResponseDTO> {
        if (snapshotIds.isEmpty()) {
            return emptyList()
        }

        val modes = satelliteModeRepository.findAllBySnapshot_IdInOrderByStartTimeAsc(snapshotIds.distinct())
        val bookedSlotsByModeId = bookedSlotsByModeId(modes.mapNotNull { it.id })
        val cellNumsByModeId = cellNumsByModeId(modes.mapNotNull { it.id })
        return modes.map { mode ->
            satelliteModeMapper.toResponse(
                entity = mode,
                bookedSlotIds = bookedSlotsByModeId[mode.id].orEmpty(),
                cellNums = cellNumsByModeId[mode.id].orEmpty()
            )
        }
    }

    private fun bookedSlotsByModeId(modeIds: Collection<Long>): Map<Long, List<Long>> {
        if (modeIds.isEmpty()) {
            return emptyMap()
        }

        return satelliteModeBookedSlotRepository.findAllByMode_IdIn(modeIds)
            .groupBy(
                keySelector = { it.mode?.id ?: -1L },
                valueTransform = { it.bookedSlotId }
            )
            .mapValues { (_, bookedSlotIds) ->
                bookedSlotIds.filter { it > 0 }.distinct()
            }
    }

    private fun cellNumsByModeId(modeIds: Collection<Long>): Map<Long, List<Long>> {
        if (modeIds.isEmpty()) {
            return emptyMap()
        }

        return satelliteModeCellRepository.findAllByMode_IdIn(modeIds)
            .groupBy(
                keySelector = { it.mode?.id ?: -1L },
                valueTransform = { it.cellNum }
            )
            .mapValues { (_, cellNums) ->
                cellNums.filter { it > 0 }.distinct().sorted()
            }
    }

    private fun buildSnapshotSpecification(
        satelliteId: Long?,
        intervalStart: LocalDateTime?,
        intervalEnd: LocalDateTime?,
        activeOnly: Boolean
    ): Specification<space.nstart.pcp.pcp_satellites_service.entity.SatelliteModeSnapshotEntity> =
        Specification { root, _, criteriaBuilder ->
            val predicates = mutableListOf<jakarta.persistence.criteria.Predicate>()
            satelliteId?.let { predicates += criteriaBuilder.equal(root.get<Long>("satelliteId"), it) }
            intervalStart?.let { predicates += criteriaBuilder.equal(root.get<LocalDateTime>("intervalStart"), it) }
            intervalEnd?.let { predicates += criteriaBuilder.equal(root.get<LocalDateTime>("intervalEnd"), it) }
            if (activeOnly) {
                predicates += criteriaBuilder.isTrue(root.get("isActive"))
            }
            criteriaBuilder.and(*predicates.toTypedArray())
        }

    private fun validateInterval(intervalStart: LocalDateTime, intervalEnd: LocalDateTime) {
        if (intervalEnd.isBefore(intervalStart)) {
            throw CustomValidationException("Параметр intervalEnd должен быть больше или равен intervalStart")
        }
    }

    private fun touchedDateRange(intervalStart: LocalDateTime, intervalEnd: LocalDateTime): List<LocalDate> {
        val lastTouchedDate = when {
            intervalEnd.isEqual(intervalStart) -> intervalStart.toLocalDate()
            intervalEnd.toLocalTime() == LocalTime.MIDNIGHT -> intervalEnd.minusNanos(1).toLocalDate()
            else -> intervalEnd.minusNanos(1).toLocalDate()
        }
        return generateSequence(intervalStart.toLocalDate()) { current ->
            current.takeIf { it < lastTouchedDate }?.plusDays(1)
        }.toList()
    }

    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<LocalDate, Double>> {
        if (!survey.timeStop.isAfter(survey.time)) {
            return listOf(survey.time.toLocalDate() to survey.duration)
        }

        val durations = mutableListOf<Pair<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
    }
}
