package space.nstart.pcp.slots_service.model

import org.locationtech.jts.geom.Envelope
import org.locationtech.jts.geom.Geometry
import org.slf4j.Logger
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotStatus
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotDTO
import space.nstart.pcp.slots_service.configuration.CustomErrorException
import space.nstart.pcp.slots_service.model.satellite.AbstractSatellite
import java.time.LocalDateTime
import java.util.PriorityQueue
import kotlin.jvm.Throws
import kotlin.math.abs
import kotlin.system.measureTimeMillis

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

    private data class QueueItem(
        val candidateId: Int,
        val gainArea: Double,
        val tn: LocalDateTime
    )

    private val support = CoverageSolverSupport(logger, satellitesById)

    @Throws(CustomErrorException::class)
    fun select(targetWkt: String, slots: List<SlotDTO>): List<SlotDTO> {
        lateinit var slotChains: List<CoverageSlotChain>
        val mergeChainsMs = measureTimeMillis {
            slotChains = mergeSlotsToChains(slots)
        }
        logger.info("Слотов после объединения в цепочки ${slotChains.size}")

//        if (slotChains.size > 25000)
//            throw CustomErrorException("Для расчета покрытия получено больше 5000 слотов! Расчет отменен")

        if (slotChains.isEmpty()) {
            logger.info(
                "coverageSolver metrics: inputSlots={}, chains=0, candidates=0, resultSlots=0, mergeChainsMs={}, totalMs={}",
                slots.size,
                mergeChainsMs,
                mergeChainsMs
            )
            return emptyList()
        }

        lateinit var targetGeometry: Geometry
        val parseTargetMs = measureTimeMillis {
            targetGeometry = support.parseTargetGeometry(targetWkt)
        }
        if (targetGeometry.isEmpty || targetGeometry.area <= CoverageSolverSupport.AREA_EPS) {
            logger.info(
                "coverageSolver metrics: inputSlots={}, chains={}, candidates=0, resultSlots=0, mergeChainsMs={}, parseTargetMs={}, totalMs={}",
                slots.size,
                slotChains.size,
                mergeChainsMs,
                parseTargetMs,
                mergeChainsMs + parseTargetMs
            )
            return emptyList()
        }

        lateinit var preparedCandidates: Pair<List<CoverageCandidate>, Map<CoverageGroupKey, CoverageGroup>>
        val buildCandidatesMs = measureTimeMillis {
            preparedCandidates = support.buildCandidates(slotChains, targetGeometry)
        }
        val candidates = preparedCandidates.first
        if (candidates.isEmpty()) {
            logger.info(
                "coverageSolver metrics: inputSlots={}, chains={}, candidates=0, resultSlots=0, mergeChainsMs={}, parseTargetMs={}, buildCandidatesMs={}, totalMs={}",
                slots.size,
                slotChains.size,
                mergeChainsMs,
                parseTargetMs,
                buildCandidatesMs,
                mergeChainsMs + parseTargetMs + buildCandidatesMs
            )
            return emptyList()
        }
        val groupsByKey = preparedCandidates.second

        lateinit var satelliteCandidates: Map<Long, CoverageSatelliteCandidates>
        val buildIndexMs = measureTimeMillis {
            satelliteCandidates = support.buildSatelliteCandidatesIndex(candidates)
        }

        val active = BooleanArray(candidates.size) { true }
        val selected = mutableListOf<CoverageCandidate>()
        var uncovered = targetGeometry
        var uncoveredEnvelope = targetGeometry.envelopeInternal
        var pqPolls = 0
        var lazyRequeues = 0
        var zeroGainDeactivated = 0
        var envelopeMissDeactivated = 0
        var acceptedCandidates = 0
        var conflictDeactivateCalls = 0

        val pq = PriorityQueue<QueueItem>(compareByDescending<QueueItem> { it.gainArea }
            .thenBy { it.tn })

        val mainLoopMs = measureTimeMillis {
            for (candidate in candidates) {
                if (candidate.slot.state == SlotStatus.BOOKED) {
                    selected.add(candidate)
                    acceptedCandidates++
                    uncovered = support.safeDifference(uncovered, candidate.coveredGeometry)
                    uncoveredEnvelope = uncovered.envelopeInternal
                    active[candidate.id] = false
                } else if (candidate.slot.state == SlotStatus.AVAILABLE) {
                    pq.add(QueueItem(candidate.id, candidate.coveredGeometry.area, candidate.slot.tn))
                } else {
                    active[candidate.id] = false
                }
            }

            if (selected.isNotEmpty()) {
                conflictDeactivateCalls++
                support.deactivateConflicts(selected, satelliteCandidates, candidates, active)
            }

            while (pq.isNotEmpty()) {
                pqPolls++
                val item = pq.poll()
                if (!active[item.candidateId]) {
                    continue
                }
                val candidate = candidates[item.candidateId]
                if (!candidate.coveredEnvelope.intersects(uncoveredEnvelope)) {
                    active[item.candidateId] = false
                    envelopeMissDeactivated++
                    continue
                }

                val gainArea = support.safeIntersection(candidate.coveredGeometry, uncovered).area
                if (gainArea <= CoverageSolverSupport.AREA_EPS) {
                    active[item.candidateId] = false
                    zeroGainDeactivated++
                    continue
                }

                // lazy update if gain dropped significantly
                if (gainArea + CoverageSolverSupport.AREA_EPS < item.gainArea) {
                    lazyRequeues++
                    pq.add(QueueItem(candidate.id, gainArea, candidate.slot.tn))
                    continue
                }

                val chain = buildGreedyChain(candidate, groupsByKey, candidates, active, uncovered, uncoveredEnvelope)
                if (chain.isEmpty()) {
                    active[item.candidateId] = false
                    continue
                }

                val acceptedChain = mutableListOf<CoverageCandidate>()
                chain.forEach { chosen ->
                    if (active[chosen.id]) {
                        if (!chosen.coveredEnvelope.intersects(uncoveredEnvelope)) {
                            active[chosen.id] = false
                            envelopeMissDeactivated++
                            return@forEach
                        }
                        val gainArea = support.safeIntersection(chosen.coveredGeometry, uncovered).area
                        if (gainArea <= CoverageSolverSupport.AREA_EPS) {
                            active[chosen.id] = false
                            zeroGainDeactivated++
                            return@forEach
                        }
                        selected.add(chosen)
                        acceptedCandidates++
                        acceptedChain.add(chosen)
                        uncovered = support.safeDifference(uncovered, chosen.coveredGeometry)
                        uncoveredEnvelope = uncovered.envelopeInternal
                        active[chosen.id] = false
                    }
                }

                if (acceptedChain.isNotEmpty()) {
                    conflictDeactivateCalls++
                    support.deactivateConflicts(acceptedChain, satelliteCandidates, candidates, active)
                }

                if (uncovered.isEmpty || uncovered.area <= CoverageSolverSupport.AREA_EPS) {
                    break
                }
            }
        }

        lateinit var nonRedundantCandidates: List<CoverageCandidate>
        val removeRedundantMs = measureTimeMillis {
            nonRedundantCandidates = support.removeRedundant(selected)
        }
        lateinit var preparedSourceCoverages: Map<String, CoverageSourceSlotCoverage>
        val sourceCoverageMapMs = measureTimeMillis {
            preparedSourceCoverages = support.buildPreparedSourceCoverageMap(nonRedundantCandidates)
        }
        lateinit var resultSlots: List<SlotDTO>
        val collectUsefulSlotsMs = measureTimeMillis {
            resultSlots = support.collectUsefulSlotsFromSelectedChains(nonRedundantCandidates, targetGeometry)
        }
        lateinit var filteredResult: List<SlotDTO>
        val finalFilterMs = measureTimeMillis {
            filteredResult = support.removeNonCoveringAndFullyCoveredResultSlots(
                slots = resultSlots,
                targetGeometry = targetGeometry,
                preparedSourceCoverages = preparedSourceCoverages
            )
        }
        val totalMs = mergeChainsMs + parseTargetMs + buildCandidatesMs + buildIndexMs + mainLoopMs +
            removeRedundantMs + sourceCoverageMapMs + collectUsefulSlotsMs + finalFilterMs
        val coveragePercent = 100 * (targetGeometry.area - uncovered.area) / targetGeometry.area
        logger.info(
            "coverageSolver metrics: inputSlots={}, chains={}, candidates={}, groups={}, selectedCandidates={}, nonRedundantCandidates={}, sourceCoverageCache={}, sourceSlots={} -> {}, coveragePercent={}, pqPolls={}, lazyRequeues={}, zeroGainDeactivated={}, envelopeMissDeactivated={}, conflictDeactivateCalls={}, acceptedCandidates={}, mergeChainsMs={}, parseTargetMs={}, buildCandidatesMs={}, buildIndexMs={}, mainLoopMs={}, removeRedundantMs={}, sourceCoverageMapMs={}, collectUsefulSlotsMs={}, finalFilterMs={}, totalMs={}",
            slots.size,
            slotChains.size,
            candidates.size,
            groupsByKey.size,
            selected.size,
            nonRedundantCandidates.size,
            preparedSourceCoverages.size,
            resultSlots.size,
            filteredResult.size,
            coveragePercent,
            pqPolls,
            lazyRequeues,
            zeroGainDeactivated,
            envelopeMissDeactivated,
            conflictDeactivateCalls,
            acceptedCandidates,
            mergeChainsMs,
            parseTargetMs,
            buildCandidatesMs,
            buildIndexMs,
            mainLoopMs,
            removeRedundantMs,
            sourceCoverageMapMs,
            collectUsefulSlotsMs,
            finalFilterMs,
            totalMs
        )
        return filteredResult
            .sortedWith(compareBy<SlotDTO>(
                { it.tn },
                { it.satelliteId },
                { it.cycle },
                { it.slotNumber }
            ))
    }

    private fun mergeSlotsToChains(slots: List<SlotDTO>): List<CoverageSlotChain> =
        support.mergeSlotsToChains(slots)

    private fun createSlotChain(
        slots: List<SlotDTO>,
        chainStart: LocalDateTime,
        chainEnd: LocalDateTime,
        chainContour: String
    ): CoverageSlotChain = support.createSlotChain(slots, chainStart, chainEnd, chainContour)

    private fun assessMergeableGeometryIntersection(
        leftContour: String,
        rightContour: String,
        satelliteId: Long
    ): GeometryMergeAssessment = support.assessMergeableGeometryIntersection(leftContour, rightContour, satelliteId)

    private fun unionContoursIfPolygon(
        baseContour: String,
        contourToAdd: String,
        chainRepresentative: SlotDTO,
        candidate: SlotDTO
    ): String? = support.unionContoursIfPolygon(baseContour, contourToAdd, chainRepresentative, candidate)

    private fun buildGreedyChain(
        anchor: CoverageCandidate,
        groupsByKey: Map<CoverageGroupKey, CoverageGroup>,
        candidates: List<CoverageCandidate>,
        active: BooleanArray,
        uncovered: Geometry,
        uncoveredEnvelope: Envelope
    ): List<CoverageCandidate> {
        val group = groupsByKey[anchor.groupKey] ?: return listOf(anchor)
        val ids = group.candidateIds
        var left = anchor.groupIndex
        var right = anchor.groupIndex
        var duration = anchor.durationSeconds
        val maxDuration = anchor.satellite.maxSurveyDuration.toLong().coerceAtLeast(anchor.durationSeconds)

        val chain = mutableListOf(anchor)

        var expanded = true
        while (expanded) {
            expanded = false

            val leftCandidate = if (left > 0) candidates[ids[left - 1]] else null
            val rightCandidate = if (right < ids.lastIndex) candidates[ids[right + 1]] else null

            val chooseLeft = shouldExtend(leftCandidate, anchor, active, duration, maxDuration, uncovered, uncoveredEnvelope)
            val chooseRight = shouldExtend(rightCandidate, anchor, active, duration, maxDuration, uncovered, uncoveredEnvelope)

            val chosen = when {
                chooseLeft == null && chooseRight == null -> null
                chooseLeft != null && chooseRight == null -> chooseLeft.second
                chooseLeft == null && chooseRight != null -> chooseRight.second
                else -> if (chooseLeft!!.first >= chooseRight!!.first) chooseLeft.second else chooseRight!!.second
            }

            if (chosen != null) {
                val candidate = chosen
                chain.add(candidate)
                duration += candidate.durationSeconds
                if (candidate.id == leftCandidate?.id) {
                    left--
                } else if (candidate.id == rightCandidate?.id) {
                    right++
                }
                expanded = true
            }
        }

        return chain.sortedBy { it.slot.tn }
    }

    private fun shouldExtend(
        candidate: CoverageCandidate?,
        anchor: CoverageCandidate,
        active: BooleanArray,
        currentDuration: Long,
        maxDuration: Long,
        uncovered: Geometry,
        uncoveredEnvelope: Envelope
    ): Pair<Double, CoverageCandidate>? {
        if (candidate == null) return null
        if (!active[candidate.id]) return null
        if (!canChainNeighbours(anchor, candidate)) return null
        if (currentDuration + candidate.durationSeconds > maxDuration) return null
        if (!candidate.coveredEnvelope.intersects(uncoveredEnvelope)) return null

        val gain = support.safeIntersection(candidate.coveredGeometry, uncovered).area
        if (gain <= CoverageSolverSupport.AREA_EPS) return null

        val score = gain
        return score to candidate
    }

    private fun canChainNeighbours(left: CoverageCandidate, right: CoverageCandidate): Boolean {
        if (left.slot.satelliteId != right.slot.satelliteId) return false
        if (left.slot.cycle != right.slot.cycle) return false
        if (abs(left.slot.roll - right.slot.roll) >= CoverageSolverSupport.ROLL_EPS) return false
        return support.intervalsIntersect(
            right.slot.tn,
            right.slot.tk,
            left.blockedStart,
            left.blockedEnd
        )
    }
}
