package space.nstart.pcp.slots_service.model

import org.locationtech.jts.geom.Envelope
import org.locationtech.jts.geom.Geometry
import org.locationtech.jts.geom.GeometryFactory
import org.locationtech.jts.geom.Polygon
import org.locationtech.jts.geom.PrecisionModel
import org.locationtech.jts.index.strtree.STRtree
import org.locationtech.jts.io.WKTReader
import org.locationtech.jts.operation.overlayng.OverlayNG
import org.locationtech.jts.operation.overlayng.OverlayNGRobust
import org.locationtech.jts.precision.GeometryPrecisionReducer
import org.slf4j.Logger
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign
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.configuration.CustomErrorException
import space.nstart.pcp.slots_service.model.satellite.AbstractSatellite
import space.nstart.pcp.slots_service.util.LongitudeWrapGeometry
import java.time.Duration
import java.time.LocalDateTime

internal data class CoverageSourceSlotCoverage(
    val slot: SlotDTO,
    val coveredGeometry: Geometry,
    val coveredEnvelope: Envelope
)

internal data class CoverageGroupKey(
    val satelliteId: Long,
    val cycle: Long,
    val rollBucket: Int,
    val revolutionSign: RevolutionSign
)

internal data class CoverageGroup(
    val candidateIds: IntArray
)

internal data class CoverageSatelliteCandidates(
    val sortedCandidateIds: IntArray,
    val maxDurationSeconds: Long
)

internal data class CoverageCandidateIndexes(
    val satelliteCandidates: Map<Long, CoverageSatelliteCandidates>,
    val spatialIndex: STRtree
)

internal data class CoverageCandidate(
    val id: Int,
    val slot: SlotDTO,
    val sourceCoverages: List<CoverageSourceSlotCoverage>,
    val satellite: AbstractSatellite,
    val coveredGeometry: Geometry,
    val coveredEnvelope: Envelope,
    val blockedStart: LocalDateTime,
    val blockedEnd: LocalDateTime,
    val durationSeconds: Long,
    val groupKey: CoverageGroupKey,
    val groupIndex: Int
)

internal data class CoverageSlotChain(
    val representative: SlotDTO,
    val slots: List<SlotDTO>
)

private data class CoverageSlotChainKey(
    val satelliteId: Long,
    val cycle: Long,
    val rollBucket: Int,
    val revolutionSign: RevolutionSign
)

internal data class GeometryMergeAssessment(
    val mergeable: Boolean,
    val intersectionArea: Double? = null,
    val geometryChecked: Boolean = true
)

internal class CoverageSolverSupport(
    private val logger: Logger,
    private val satellitesById: Map<Long, AbstractSatellite>
) {

    private data class SlotCoverage(
        val id: Int,
        val slot: SlotDTO,
        val coveredGeometry: Geometry,
        val coveredEnvelope: Envelope,
        val area: Double
    )

    private val geometryFactory = GeometryFactory()
    private val wktReader = WKTReader()

    fun parseTargetGeometry(targetWkt: String): Geometry {
        val rawGeometry = try {
            LongitudeWrapGeometry.normalizeToContinuous360(wktReader.read(targetWkt))
        } catch (e: Exception) {
            throw IllegalArgumentException("Некорректный контур заявки: ${e.message}")
        }
        return normalizeGeometry(rawGeometry)
    }

    fun mergeSlotsToChains(slots: List<SlotDTO>): List<CoverageSlotChain> {
        if (slots.isEmpty()) return emptyList()

        val buckets = mutableMapOf<CoverageSlotChainKey, MutableList<SlotDTO>>()
        for (slot in slots) {
            val key = CoverageSlotChainKey(
                satelliteId = slot.satelliteId,
                cycle = slot.cycle,
                rollBucket = rollBucket(slot.roll),
                revolutionSign = slot.revolutionSign
            )
            buckets.getOrPut(key) { mutableListOf() }.add(slot)
        }

        val chains = mutableListOf<CoverageSlotChain>()
        for ((_, bucketSlots) in buckets) {
            if (bucketSlots.isEmpty()) continue
            bucketSlots.sortWith(compareBy<SlotDTO>({ it.tn }, { it.tk }, { it.slotNumber }))
            val maxChainDurationSeconds = satellitesById[bucketSlots.first().satelliteId]
                ?.maxSurveyDuration
                ?.toLong()
                ?.coerceAtLeast(0L)
                ?: Long.MAX_VALUE

            var chainStart = bucketSlots.first().tn
            var chainEnd = bucketSlots.first().tk
            var chainContour = bucketSlots.first().contour
            var chainSlots = mutableListOf(bucketSlots.first())

            for (i in 1 until bucketSlots.size) {
                val slot = bucketSlots[i]
                val candidateEnd = if (slot.tk > chainEnd) slot.tk else chainEnd
                val candidateDurationSeconds = Duration.between(chainStart, candidateEnd).seconds.coerceAtLeast(0L)
                val exceedsMaxDuration = candidateDurationSeconds > maxChainDurationSeconds

                if (intervalsIntersect(chainStart, chainEnd, slot.tn, slot.tk) && !exceedsMaxDuration) {
                    val geometryMergeAssessment = assessMergeableGeometryIntersection(
                        leftContour = chainContour,
                        rightContour = slot.contour,
                        satelliteId = chainSlots.first().satelliteId
                    )
                    if (!geometryMergeAssessment.mergeable) {
                        if (geometryMergeAssessment.geometryChecked) {
                            logChainMergeSkippedWithoutSufficientGeometry(
                                chainRepresentative = chainSlots.first(),
                                candidate = slot,
                                intersectionArea = geometryMergeAssessment.intersectionArea ?: 0.0
                            )
                        }
                        chains.add(createSlotChain(chainSlots, chainStart, chainEnd, chainContour))
                        chainStart = slot.tn
                        chainEnd = slot.tk
                        chainContour = slot.contour
                        chainSlots = mutableListOf(slot)
                        continue
                    }
                    val mergedContour = unionContoursIfPolygon(
                        baseContour = chainContour,
                        contourToAdd = slot.contour,
                        chainRepresentative = chainSlots.first(),
                        candidate = slot
                    )
                    if (mergedContour == null) {
                        chains.add(createSlotChain(chainSlots, chainStart, chainEnd, chainContour))
                        chainStart = slot.tn
                        chainEnd = slot.tk
                        chainContour = slot.contour
                        chainSlots = mutableListOf(slot)
                        continue
                    }
                    chainSlots.add(slot)
                    chainEnd = candidateEnd
                    chainContour = mergedContour
                } else {
                    chains.add(createSlotChain(chainSlots, chainStart, chainEnd, chainContour))
                    chainStart = slot.tn
                    chainEnd = slot.tk
                    chainContour = slot.contour
                    chainSlots = mutableListOf(slot)
                }
            }
            chains.add(createSlotChain(chainSlots, chainStart, chainEnd, chainContour))
        }

        return chains
    }

    fun createSlotChain(
        slots: List<SlotDTO>,
        chainStart: LocalDateTime,
        chainEnd: LocalDateTime,
        chainContour: String
    ): CoverageSlotChain {
        val first = slots.first()
        val chainState = when {
            slots.any { it.state == SlotStatus.BOOKED } -> SlotStatus.BOOKED
            slots.any { it.state == SlotStatus.AVAILABLE } -> SlotStatus.AVAILABLE
            else -> first.state
        }

        val representative = SlotDTO(
            cycle = first.cycle,
            satelliteId = first.satelliteId,
            tn = chainStart,
            tk = chainEnd,
            roll = first.roll,
            contour = chainContour,
            revolution = first.revolution,
            revolutionSign = first.revolutionSign,
            slotNumber = slots.minOf { it.slotNumber },
            state = chainState,
            latitude = first.latitude,
            longitude = first.longitude
        )

        return CoverageSlotChain(representative = representative, slots = slots.toList())
    }

    fun assessMergeableGeometryIntersection(
        leftContour: String,
        rightContour: String,
        satelliteId: Long
    ): GeometryMergeAssessment {
        return try {
            val leftGeometry = wktReader.read(leftContour)
            val rightGeometry = wktReader.read(rightContour)
            logInvalidGeometryIfNeeded(satelliteId, "chain", leftGeometry)
            logInvalidGeometryIfNeeded(satelliteId, "candidate", rightGeometry)
            val intersectionArea = leftGeometry.intersection(rightGeometry).area
            GeometryMergeAssessment(
                mergeable = intersectionArea > AREA_EPS,
                intersectionArea = intersectionArea
            )
        } catch (e: Exception) {
            logger.warn("Не удалось проверить пересечение геометрий цепочки solver: {}", e.message)
            GeometryMergeAssessment(mergeable = false, geometryChecked = false)
        }
    }

    fun unionContoursIfPolygon(
        baseContour: String,
        contourToAdd: String,
        chainRepresentative: SlotDTO,
        candidate: SlotDTO
    ): String? {
        return try {
            val unionGeometry = wktReader.read(baseContour).union(wktReader.read(contourToAdd))
            if (unionGeometry !is Polygon) {
                logger.warn(
                    "Solver chain merge skipped because union produced non-polygon geometry: satelliteId={}, geometryType={}, chainStart={}, chainEnd={}, slotStart={}, slotEnd={}, chainRoll={}, slotRoll={}, chainSlotNumber={}, slotNumber={}",
                    chainRepresentative.satelliteId,
                    unionGeometry.geometryType,
                    chainRepresentative.tn,
                    chainRepresentative.tk,
                    candidate.tn,
                    candidate.tk,
                    chainRepresentative.roll,
                    candidate.roll,
                    chainRepresentative.slotNumber,
                    candidate.slotNumber
                )
                return null
            }
            unionGeometry.toText()
        } catch (e: Exception) {
            logger.warn("Не удалось объединить геометрию цепочки solver: {}", e.message)
            null
        }
    }

    fun buildCandidates(
        slotChains: List<CoverageSlotChain>,
        targetGeometry: Geometry
    ): Pair<List<CoverageCandidate>, Map<CoverageGroupKey, CoverageGroup>> {
        val tmpCandidates = ArrayList<CoverageCandidate>(slotChains.size)
        val groupBuckets = mutableMapOf<CoverageGroupKey, MutableList<Int>>()
        val targetEnvelope = targetGeometry.envelopeInternal

        for (chain in slotChains) {
            val slot = chain.representative
            val satellite = satellitesById[slot.satelliteId] ?: continue
            var chainCoveredGeometry: Geometry? = null
            val sourceCoverages = mutableListOf<CoverageSourceSlotCoverage>()

            for (sourceSlot in chain.slots) {
                val slotGeometry = try {
                    normalizeGeometry(
                        LongitudeWrapGeometry.alignToReference(
                            wktReader.read(sourceSlot.contour),
                            targetGeometry
                        )
                    )
                } catch (e: Exception) {
                    logger.warn("Пропуск слота {}:{}: {}", sourceSlot.satelliteId, sourceSlot.slotNumber, e.message)
                    continue
                }
                if (!slotGeometry.envelopeInternal.intersects(targetEnvelope)) {
                    continue
                }
                val coveredGeometry = safeIntersection(slotGeometry, targetGeometry)
                if (coveredGeometry.isEmpty || coveredGeometry.area <= AREA_EPS) {
                    continue
                }
                sourceCoverages.add(
                    CoverageSourceSlotCoverage(
                        slot = sourceSlot,
                        coveredGeometry = coveredGeometry,
                        coveredEnvelope = coveredGeometry.envelopeInternal
                    )
                )

                chainCoveredGeometry = if (chainCoveredGeometry == null) {
                    coveredGeometry
                } else {
                    safeUnion(chainCoveredGeometry!!, coveredGeometry)
                }
            }

            val coveredGeometry = chainCoveredGeometry ?: continue
            if (coveredGeometry.isEmpty || coveredGeometry.area <= AREA_EPS) {
                continue
            }
            val rollBucket = rollBucket(slot.roll)
            val groupKey = CoverageGroupKey(
                satelliteId = slot.satelliteId,
                cycle = slot.cycle,
                rollBucket = rollBucket,
                revolutionSign = slot.revolutionSign
            )
            val candidateId = tmpCandidates.size
            val mmi = satellite.mmi
            val candidate = CoverageCandidate(
                id = candidateId,
                slot = slot,
                sourceCoverages = sourceCoverages,
                satellite = satellite,
                coveredGeometry = coveredGeometry,
                coveredEnvelope = coveredGeometry.envelopeInternal,
                blockedStart = slot.tn.minusSeconds(mmi),
                blockedEnd = slot.tk.plusSeconds(mmi),
                durationSeconds = Duration.between(slot.tn, slot.tk).seconds.coerceAtLeast(0L),
                groupKey = groupKey,
                groupIndex = 0
            )
            tmpCandidates.add(candidate)
            groupBuckets.getOrPut(groupKey) { mutableListOf() }.add(candidateId)
        }

        val finalizedCandidates = tmpCandidates.toMutableList()
        val groups = mutableMapOf<CoverageGroupKey, CoverageGroup>()

        for ((key, ids) in groupBuckets) {
            ids.sortWith(compareBy<Int> { finalizedCandidates[it].slot.tn }
                .thenBy { finalizedCandidates[it].slot.tk }
                .thenBy { finalizedCandidates[it].slot.slotNumber })
            ids.forEachIndexed { index, id ->
                val old = finalizedCandidates[id]
                finalizedCandidates[id] = old.copy(groupIndex = index)
            }
            groups[key] = CoverageGroup(ids.toIntArray())
        }

        return finalizedCandidates to groups
    }

    fun buildSatelliteCandidatesIndex(candidates: List<CoverageCandidate>): Map<Long, CoverageSatelliteCandidates> {
        val candidatesBySatellite = mutableMapOf<Long, MutableList<Int>>()
        val maxDurationBySatellite = mutableMapOf<Long, Long>()

        for (candidate in candidates) {
            val satelliteId = candidate.slot.satelliteId
            candidatesBySatellite.getOrPut(satelliteId) { mutableListOf() }.add(candidate.id)
            val maxDuration = maxDurationBySatellite[satelliteId] ?: 0L
            if (candidate.durationSeconds > maxDuration) {
                maxDurationBySatellite[satelliteId] = candidate.durationSeconds
            }
        }

        val result = mutableMapOf<Long, CoverageSatelliteCandidates>()
        for ((satelliteId, ids) in candidatesBySatellite) {
            ids.sortWith(compareBy<Int> { candidates[it].slot.tn }
                .thenBy { candidates[it].slot.tk }
                .thenBy { candidates[it].slot.slotNumber })
            result[satelliteId] = CoverageSatelliteCandidates(
                sortedCandidateIds = ids.toIntArray(),
                maxDurationSeconds = maxDurationBySatellite[satelliteId] ?: 0L
            )
        }

        return result
    }

    fun buildCandidateIndexes(candidates: List<CoverageCandidate>): CoverageCandidateIndexes {
        val spatialIndex = STRtree(candidates.size)
        for (candidate in candidates) {
            spatialIndex.insert(candidate.coveredEnvelope, candidate.id)
        }
        spatialIndex.build()
        return CoverageCandidateIndexes(
            satelliteCandidates = buildSatelliteCandidatesIndex(candidates),
            spatialIndex = spatialIndex
        )
    }

    fun deactivateConflicts(
        selected: List<CoverageCandidate>,
        candidatesBySatellite: Map<Long, CoverageSatelliteCandidates>,
        candidates: List<CoverageCandidate>,
        active: BooleanArray
    ) {
        if (selected.isEmpty()) return
        for (sel in selected) {
            val satelliteCandidates = candidatesBySatellite[sel.slot.satelliteId] ?: continue
            val ids = satelliteCandidates.sortedCandidateIds
            if (ids.isEmpty()) continue

            val startBound = sel.blockedStart.minusSeconds(satelliteCandidates.maxDurationSeconds)
            val from = lowerBoundByStart(ids, candidates, startBound)
            val to = upperBoundByStart(ids, candidates, sel.blockedEnd)

            for (idx in from until to) {
                val candidateId = ids[idx]
                if (!active[candidateId]) continue
                val candidate = candidates[candidateId]
                if (intervalsIntersect(candidate.slot.tn, candidate.slot.tk, sel.blockedStart, sel.blockedEnd)) {
                    active[candidate.id] = false
                }
            }
        }
    }

    fun removeRedundant(selected: List<CoverageCandidate>): List<CoverageCandidate> {
        if (selected.isEmpty()) return emptyList()
        val kept = selected.toMutableList()
        val targetArea = unionGeometries(kept.map { it.coveredGeometry }).area

        var changed = true
        while (changed) {
            changed = false
            val removable = kept
                .filter { it.slot.state != SlotStatus.BOOKED }
                .sortedBy { it.coveredGeometry.area }
            val indexById = kept
                .withIndex()
                .associate { (index, candidate) -> candidate.id to index }
            val prefixUnions = buildPrefixUnions(kept.map { it.coveredGeometry })
            val suffixUnions = buildSuffixUnions(kept.map { it.coveredGeometry })

            for (candidate in removable) {
                val candidateIndex = indexById[candidate.id] ?: continue
                val areaWithout = safeUnion(
                    prefixUnions[candidateIndex],
                    suffixUnions[candidateIndex + 1]
                ).area
                if (targetArea - areaWithout <= AREA_EPS) {
                    kept.removeIf { it.id == candidate.id }
                    changed = true
                    break
                }
            }
        }
        return kept
    }

    fun collectUsefulSlotsFromSelectedChains(
        selectedCandidates: List<CoverageCandidate>,
        targetGeometry: Geometry
    ): List<SlotDTO> {
        if (selectedCandidates.isEmpty()) return emptyList()

        val orderedCandidates = selectedCandidates.sortedWith(compareBy<CoverageCandidate>(
            { it.slot.tn },
            { it.slot.satelliteId },
            { it.slot.cycle },
            { it.slot.slotNumber }
        ))

        val resultSlots = mutableListOf<SlotDTO>()
        val seen = hashSetOf<String>()
        var uncovered = targetGeometry
        var uncoveredEnvelope = targetGeometry.envelopeInternal

        for (candidate in orderedCandidates) {
            val orderedSources = candidate.sourceCoverages.sortedWith(compareBy<CoverageSourceSlotCoverage>(
                { it.slot.tn },
                { it.slot.satelliteId },
                { it.slot.cycle },
                { it.slot.slotNumber }
            ))

            for (source in orderedSources) {
                if (!source.coveredEnvelope.intersects(uncoveredEnvelope)) continue
                val gainArea = safeIntersection(source.coveredGeometry, uncovered).area
                if (gainArea <= AREA_EPS) continue

                val key = slotKey(source.slot)
                if (seen.add(key)) {
                    resultSlots.add(source.slot)
                }

                uncovered = safeDifference(uncovered, source.coveredGeometry)
                uncoveredEnvelope = uncovered.envelopeInternal
                if (uncovered.isEmpty || uncovered.area <= AREA_EPS) {
                    return resultSlots
                }
            }
        }

        return resultSlots
    }

    fun buildPreparedSourceCoverageMap(selectedCandidates: List<CoverageCandidate>): Map<String, CoverageSourceSlotCoverage> {
        if (selectedCandidates.isEmpty()) {
            return emptyMap()
        }
        val result = linkedMapOf<String, CoverageSourceSlotCoverage>()
        for (candidate in selectedCandidates) {
            for (sourceCoverage in candidate.sourceCoverages) {
                result.putIfAbsent(slotKey(sourceCoverage.slot), sourceCoverage)
            }
        }
        return result
    }

    fun removeNonCoveringAndFullyCoveredResultSlots(
        slots: List<SlotDTO>,
        targetGeometry: Geometry,
        preparedSourceCoverages: Map<String, CoverageSourceSlotCoverage> = emptyMap()
    ): List<SlotDTO> {
        if (slots.isEmpty()) return emptyList()

        val targetEnvelope = targetGeometry.envelopeInternal
        val slotCoverages = ArrayList<SlotCoverage>(slots.size)

        for (slot in slots) {
            val preparedCoverage = preparedSourceCoverages[slotKey(slot)]
            val coveredGeometry = if (preparedCoverage != null) {
                preparedCoverage.coveredGeometry
            } else {
                try {
                    val slotGeometry = normalizeGeometry(
                        LongitudeWrapGeometry.alignToReference(
                            wktReader.read(slot.contour),
                            targetGeometry
                        )
                    )
                    if (!slotGeometry.envelopeInternal.intersects(targetEnvelope)) {
                        geometryFactory.createPolygon()
                    } else {
                        safeIntersection(slotGeometry, targetGeometry)
                    }
                } catch (e: Exception) {
                    logger.warn("Исключен слот {}:{}: некорректный контур ({})", slot.satelliteId, slot.slotNumber, e.message)
                    geometryFactory.createPolygon()
                }
            }

            if (coveredGeometry.isEmpty || coveredGeometry.area <= AREA_EPS) continue
            slotCoverages.add(
                SlotCoverage(
                    id = slotCoverages.size,
                    slot = slot,
                    coveredGeometry = coveredGeometry,
                    coveredEnvelope = coveredGeometry.envelopeInternal,
                    area = coveredGeometry.area
                )
            )
        }

        if (slotCoverages.size <= 1) return slotCoverages.map { it.slot }

        val index = STRtree(slotCoverages.size)
        for (coverage in slotCoverages) {
            index.insert(coverage.coveredEnvelope, coverage)
        }
        index.build()

        val active = BooleanArray(slotCoverages.size) { true }
        val orderedByArea = slotCoverages
            .indices
            .sortedWith(compareBy<Int> { slotCoverages[it].area }
                .thenBy { slotCoverages[it].slot.tn }
                .thenBy { slotCoverages[it].slot.satelliteId }
                .thenBy { slotCoverages[it].slot.cycle }
                .thenBy { slotCoverages[it].slot.slotNumber })

        for (idx in orderedByArea) {
            if (!active[idx]) continue
            val candidate = slotCoverages[idx]

            val queried = index.query(candidate.coveredEnvelope)
            val coverers = ArrayList<SlotCoverage>(queried.size)
            for (item in queried) {
                val coverage = item as? SlotCoverage ?: continue
                if (coverage.id == candidate.id) continue
                if (!active[coverage.id]) continue
                if (!coverage.coveredEnvelope.intersects(candidate.coveredEnvelope)) continue
                coverers.add(coverage)
            }
            if (coverers.isEmpty()) continue

            var fullyCovered = false
            for (coverer in coverers) {
                if (coverer.coveredGeometry.covers(candidate.coveredGeometry)) {
                    fullyCovered = true
                    break
                }
            }
            if (fullyCovered) {
                active[idx] = false
                continue
            }

            val orderedCoverers = coverers.sortedByDescending {
                envelopeIntersectionArea(it.coveredEnvelope, candidate.coveredEnvelope)
            }

            var unionGeometry: Geometry? = null
            val unionEnvelope = Envelope()
            for (coverer in orderedCoverers) {
                unionGeometry = if (unionGeometry == null) {
                    coverer.coveredGeometry
                } else {
                    safeUnion(unionGeometry!!, coverer.coveredGeometry)
                }

                unionEnvelope.expandToInclude(coverer.coveredEnvelope)
                if (!unionEnvelope.contains(candidate.coveredEnvelope)) continue

                val uncoveredPart = safeDifference(candidate.coveredGeometry, unionGeometry!!)
                if (uncoveredPart.isEmpty || uncoveredPart.area <= AREA_EPS) {
                    active[idx] = false
                    break
                }
            }
        }

        return slotCoverages
            .filter { active[it.id] }
            .map { it.slot }
    }

    fun unionGeometries(geometries: List<Geometry>): Geometry {
        if (geometries.isEmpty()) return geometryFactory.createPolygon()
        var union = geometries.first()
        for (i in 1 until geometries.size) {
            union = safeUnion(union, geometries[i])
        }
        return union
    }

    fun safeIntersection(a: Geometry, b: Geometry): Geometry {
        if (a.isEmpty || b.isEmpty) return geometryFactory.createPolygon()
        if (!a.envelopeInternal.intersects(b.envelopeInternal)) return geometryFactory.createPolygon()
        return safeOverlay(a, b, OverlayNG.INTERSECTION)
    }

    fun safeDifference(a: Geometry, b: Geometry): Geometry {
        if (a.isEmpty) return geometryFactory.createPolygon()
        if (b.isEmpty) return a
        if (!a.envelopeInternal.intersects(b.envelopeInternal)) return a
        return safeOverlay(a, b, OverlayNG.DIFFERENCE)
    }

    fun safeUnion(a: Geometry, b: Geometry): Geometry {
        if (a.isEmpty) return b
        if (b.isEmpty) return a
        return safeOverlay(a, b, OverlayNG.UNION)
    }

    fun normalizeGeometry(geometry: Geometry): Geometry {
        if (geometry.isEmpty) return geometryFactory.createPolygon()
        return if (geometry.isValid) geometry else geometry.buffer(0.0)
    }

    fun intervalsIntersect(
        start1: LocalDateTime,
        end1: LocalDateTime,
        start2: LocalDateTime,
        end2: LocalDateTime
    ) = start1 <= end2 && start2 <= end1

    fun rollBucket(roll: Double): Int = (roll / ROLL_EPS).toInt()

    private fun lowerBoundByStart(ids: IntArray, candidates: List<CoverageCandidate>, threshold: LocalDateTime): Int {
        var low = 0
        var high = ids.size
        while (low < high) {
            val mid = (low + high) ushr 1
            if (candidates[ids[mid]].slot.tn < threshold) {
                low = mid + 1
            } else {
                high = mid
            }
        }
        return low
    }

    private fun upperBoundByStart(ids: IntArray, candidates: List<CoverageCandidate>, threshold: LocalDateTime): Int {
        var low = 0
        var high = ids.size
        while (low < high) {
            val mid = (low + high) ushr 1
            if (candidates[ids[mid]].slot.tn <= threshold) {
                low = mid + 1
            } else {
                high = mid
            }
        }
        return low
    }

    private fun slotKey(slot: SlotDTO): String =
        "${slot.satelliteId}_${slot.cycle}_${slot.slotNumber}_${slot.tn}_${slot.tk}_${slot.roll}"

    private fun buildPrefixUnions(geometries: List<Geometry>): List<Geometry> {
        val prefix = ArrayList<Geometry>(geometries.size + 1)
        prefix.add(geometryFactory.createPolygon())
        for (geometry in geometries) {
            prefix.add(safeUnion(prefix.last(), geometry))
        }
        return prefix
    }

    private fun buildSuffixUnions(geometries: List<Geometry>): List<Geometry> {
        val suffix = MutableList(geometries.size + 1) { geometryFactory.createPolygon() as Geometry }
        for (index in geometries.indices.reversed()) {
            suffix[index] = safeUnion(geometries[index], suffix[index + 1])
        }
        return suffix
    }

    private fun envelopeIntersectionArea(a: Envelope, b: Envelope): Double {
        val minX = maxOf(a.minX, b.minX)
        val maxX = minOf(a.maxX, b.maxX)
        if (maxX <= minX) return 0.0

        val minY = maxOf(a.minY, b.minY)
        val maxY = minOf(a.maxY, b.maxY)
        if (maxY <= minY) return 0.0

        return (maxX - minX) * (maxY - minY)
    }

    private fun safeOverlay(a: Geometry, b: Geometry, opCode: Int): Geometry {
        val g1 = normalizeGeometry(a)
        val g2 = normalizeGeometry(b)

        return try {
            normalizeGeometry(OverlayNGRobust.overlay(g1, g2, opCode))
        } catch (_: Exception) {
            val snapped1 = snapGeometry(g1, 1e-6)
            val snapped2 = snapGeometry(g2, 1e-6)
            try {
                normalizeGeometry(OverlayNGRobust.overlay(snapped1, snapped2, opCode))
            } catch (_: Exception) {
                val buffered1 = normalizeGeometry(snapped1.buffer(0.0))
                val buffered2 = normalizeGeometry(snapped2.buffer(0.0))
                normalizeGeometry(OverlayNGRobust.overlay(buffered1, buffered2, opCode))
            }
        }
    }

    private fun snapGeometry(geometry: Geometry, tolerance: Double): Geometry {
        val scale = 1.0 / tolerance
        return normalizeGeometry(GeometryPrecisionReducer.reduce(geometry, PrecisionModel(scale)))
    }

    private fun logInvalidGeometryIfNeeded(
        satelliteId: Long,
        geometryRole: String,
        geometry: Geometry
    ) {
        if (geometry.isValid) {
            return
        }
        logger.warn(
            "Solver chain merge geometry is invalid: satelliteId={}, geometryRole={}, geometryType={}",
            satelliteId,
            geometryRole,
            geometry.geometryType
        )
    }

    private fun logChainMergeSkippedWithoutSufficientGeometry(
        chainRepresentative: SlotDTO,
        candidate: SlotDTO,
        intersectionArea: Double
    ) {
        logger.warn(
            "Solver chain merge skipped because contours lack sufficient areal intersection despite temporal overlap: satelliteId={}, intersectionArea={}, epsilon={}, chainStart={}, chainEnd={}, slotStart={}, slotEnd={}, chainRoll={}, slotRoll={}, chainSlotNumber={}, slotNumber={}",
            chainRepresentative.satelliteId,
            intersectionArea,
            AREA_EPS,
            chainRepresentative.tn,
            chainRepresentative.tk,
            candidate.tn,
            candidate.tk,
            chainRepresentative.roll,
            candidate.roll,
            chainRepresentative.slotNumber,
            candidate.slotNumber
        )
    }

    companion object {
        const val AREA_EPS = 1e-8
        const val ROLL_EPS = 0.01
    }
}
