package space.nstart.pcp.slots_service.service

import space.nstart.pcp.pcp_types_lib.dto.requests.slots.OccupiedIntervalDTO
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.slots_service.model.satellite.AbstractSatellite
import java.time.Duration
import kotlin.math.abs

internal object OccupiedIntervalPrefilter {

    private const val ROLL_EPSILON = 0.01

    fun normalizeBySatellite(
        occupiedIntervals: List<OccupiedIntervalDTO>,
        satellitesById: Map<Long, AbstractSatellite>
    ): Map<Long, List<OccupiedIntervalDTO>> =
        occupiedIntervals
            .groupBy { it.satelliteId }
            .mapValues { (satelliteId, intervals) ->
                val satellite = satellitesById[satelliteId] ?: return@mapValues emptyList()
                normalize(intervals, satellite)
            }
            .filterValues { it.isNotEmpty() }

    fun normalize(
        occupiedIntervals: List<OccupiedIntervalDTO>,
        satellite: AbstractSatellite
    ): List<OccupiedIntervalDTO> {
        if (occupiedIntervals.isEmpty()) {
            return emptyList()
        }

        val normalized = mutableListOf<OccupiedIntervalDTO>()
        occupiedIntervals
            .sortedWith(compareBy<OccupiedIntervalDTO> { it.roll }.thenBy { it.startTime }.thenBy { it.endTime })
            .forEach { interval ->
                val current = normalized.lastOrNull()
                if (current != null &&
                    sameRoll(current.roll, interval.roll) &&
                    interval.startTime <= current.endTime.plusSeconds(satellite.mmi)
                ) {
                    normalized[normalized.lastIndex] = current.copy(
                        startTime = minOf(current.startTime, interval.startTime),
                        endTime = maxOf(current.endTime, interval.endTime),
                        source = current.source ?: interval.source,
                        modeId = current.modeId ?: interval.modeId
                    )
                } else {
                    normalized += interval
                }
            }
        return normalized
    }

    fun filterCandidates(
        candidateSlots: List<SlotDTO>,
        occupiedIntervals: List<OccupiedIntervalDTO>,
        satellite: AbstractSatellite
    ): List<SlotDTO> {
        if (candidateSlots.isEmpty() || occupiedIntervals.isEmpty()) {
            return candidateSlots
        }

        return candidateSlots.filter { candidate ->
            isAllowed(candidate, occupiedIntervals, satellite)
        }
    }

    private fun isAllowed(
        candidate: SlotDTO,
        occupiedIntervals: List<OccupiedIntervalDTO>,
        satellite: AbstractSatellite
    ): Boolean {
        val insideStrictReservedInterval = candidate.state != SlotStatus.BOOKED &&
            occupiedIntervals.any { occupied ->
                isStrictReservedSource(occupied.source) &&
                    isInside(candidate.tn, candidate.tk, occupied.startTime, occupied.endTime)
            }
        if (insideStrictReservedInterval) {
            return false
        }

        val conflictingDifferentRoll = occupiedIntervals.any { occupied ->
            !sameRoll(candidate.roll, occupied.roll) &&
                intersectsWithMmi(candidate.tn, candidate.tk, occupied.startTime, occupied.endTime, satellite.mmi)
        }
        if (conflictingDifferentRoll) {
            return false
        }

        val sameRollChains = occupiedIntervals.filter { occupied ->
            sameRoll(candidate.roll, occupied.roll) &&
                intersectsWithMmi(candidate.tn, candidate.tk, occupied.startTime, occupied.endTime, satellite.mmi)
        }
        if (sameRollChains.isEmpty()) {
            return true
        }

        val mergedStart = sameRollChains.minOf { it.startTime }.coerceAtMost(candidate.tn)
        val mergedEnd = sameRollChains.maxOf { it.endTime }.coerceAtLeast(candidate.tk)
        val mergedDurationSeconds = Duration.between(mergedStart, mergedEnd).seconds
        return mergedDurationSeconds <= satellite.maxSurveyDuration.toLong()
    }

    private fun intersectsWithMmi(
        start1: java.time.LocalDateTime,
        end1: java.time.LocalDateTime,
        start2: java.time.LocalDateTime,
        end2: java.time.LocalDateTime,
        mmi: Long
    ): Boolean = start1 <= end2.plusSeconds(mmi) && end1 >= start2.minusSeconds(mmi)

    private fun isInside(
        start: java.time.LocalDateTime,
        end: java.time.LocalDateTime,
        containerStart: java.time.LocalDateTime,
        containerEnd: java.time.LocalDateTime
    ): Boolean = !start.isBefore(containerStart) && !end.isAfter(containerEnd)

    private fun isStrictReservedSource(source: String?): Boolean =
        source.equals("SLOTS", ignoreCase = true)

    private fun sameRoll(left: Double, right: Double): Boolean = abs(left - right) <= ROLL_EPSILON
}
