package space.nstart.pcp.pcp_satellites_service.service

import org.locationtech.jts.geom.Geometry
import org.locationtech.jts.geom.Polygon
import org.locationtech.jts.io.WKTReader
import org.locationtech.jts.io.WKTWriter
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import space.nstart.pcp.pcp_satellites_service.entity.SatelliteModeSource
import space.nstart.pcp.pcp_satellites_service.model.BLS
import space.nstart.pcp.pcp_satellites_service.model.SatelliteModel
import space.nstart.pcp.pcp_satellites_service.model.mode.SurveyMode
import java.time.Duration
import java.time.LocalDate
import java.time.LocalDateTime
import kotlin.math.abs
import kotlin.math.ceil

@Service
class SurveyPlacementService {

    data class PlacementDecision(
        val accepted: Boolean,
        val appliedSurvey: SurveyMode? = null,
        val adjustedCandidate: SurveyMode? = null,
        val absorbedSurveys: List<SurveyMode> = emptyList(),
        val trimmed: Boolean = false,
        val rejectionReason: String? = null
    )

    private data class PlacementCandidate(
        val adjustedCandidate: SurveyMode,
        val absorbedSurveys: List<SurveyMode>,
        val mergedSurvey: SurveyMode,
        val resultingSurveys: List<SurveyMode>
    )

    private data class PlacementValidation(
        val accepted: Boolean,
        val reason: String? = null
    )

    private data class GeometryMergeAssessment(
        val mergeable: Boolean,
        val intersectionArea: Double? = null,
        val touchesOrIntersects: Boolean = false,
        val distance: Double? = null,
        val geometryChecked: Boolean = true
    )

    companion object {
        private const val ROLL_EPSILON = 0.01
        private const val GEOMETRY_INTERSECTION_AREA_EPSILON = 1e-8
        private const val GEOMETRY_DISTANCE_EPSILON = 1e-6
        private const val MERGE_MAX_TIME_GAP_SECONDS = 1L
    }

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

    fun tryPlaceSurvey(
        existingSurveys: MutableList<SurveyMode>,
        candidateSurvey: SurveyMode,
        satellite: SatelliteModel
    ): PlacementDecision {
        val constraints = satellite.bls
        val initialPlacement = buildPlacementCandidate(existingSurveys, candidateSurvey, satellite.satelliteId)
        val initialValidation = validatePlacement(initialPlacement, constraints)
        if (initialValidation.accepted) {
            applyPlacement(existingSurveys, initialPlacement)
            return PlacementDecision(
                accepted = true,
                appliedSurvey = initialPlacement.mergedSurvey,
                adjustedCandidate = initialPlacement.adjustedCandidate,
                absorbedSurveys = initialPlacement.absorbedSurveys
            )
        }

        val adjustedPlacement = adjustPlacementToConstraints(existingSurveys, candidateSurvey, constraints, satellite.satelliteId)
            ?: return PlacementDecision(
                accepted = false,
                rejectionReason = initialValidation.reason ?: "placement_rejected"
            )
        val adjustedValidation = validatePlacement(adjustedPlacement, constraints)
        if (!adjustedValidation.accepted) {
            return PlacementDecision(
                accepted = false,
                rejectionReason = adjustedValidation.reason ?: "placement_rejected"
            )
        }

        applyPlacement(existingSurveys, adjustedPlacement)
        return PlacementDecision(
            accepted = true,
            appliedSurvey = adjustedPlacement.mergedSurvey,
            adjustedCandidate = adjustedPlacement.adjustedCandidate,
            absorbedSurveys = adjustedPlacement.absorbedSurveys,
            trimmed = adjustedPlacement.adjustedCandidate.timeStop != candidateSurvey.timeStop
        )
    }

    fun mergeSurvey(
        existingSurveys: MutableList<SurveyMode>,
        candidateSurvey: SurveyMode,
        satellite: SatelliteModel
    ): SurveyMode {
        val placement = buildPlacementCandidate(existingSurveys, candidateSurvey, satellite.satelliteId)
        applyPlacement(existingSurveys, placement)
        return placement.mergedSurvey
    }

    private fun buildPlacementCandidate(
        existingSurveys: List<SurveyMode>,
        candidateSurvey: SurveyMode,
        satelliteId: Long
    ): PlacementCandidate {
        val mergeChain = buildMergeChain(existingSurveys, candidateSurvey, satelliteId)
        val resultingSurveys = existingSurveys
            .filterNot { mergeChain.absorbedSurveys.contains(it) }
            .plus(mergeChain.mergedSurvey)
            .sortedBy { it.time }

        return PlacementCandidate(
            adjustedCandidate = candidateSurvey,
            absorbedSurveys = mergeChain.absorbedSurveys,
            mergedSurvey = mergeChain.mergedSurvey,
            resultingSurveys = resultingSurveys
        )
    }

    private fun adjustPlacementToConstraints(
        existingSurveys: List<SurveyMode>,
        candidateSurvey: SurveyMode,
        constraints: BLS,
        satelliteId: Long
    ): PlacementCandidate? {
        val originalDurationSeconds = Duration.between(candidateSurvey.time, candidateSurvey.timeStop).seconds
        if (originalDurationSeconds <= 0) {
            return null
        }

        val minDurationSeconds = ceil(constraints.durationMin).toLong().coerceAtLeast(1L)
        var low = minDurationSeconds
        var high = originalDurationSeconds
        var bestPlacement: PlacementCandidate? = null

        while (low <= high) {
            val mid = (low + high) / 2
            val adjustedCandidate = trimCandidate(candidateSurvey, mid)
            val placement = buildPlacementCandidate(existingSurveys, adjustedCandidate, satelliteId)
            val validation = validatePlacement(placement, constraints)
            if (validation.accepted) {
                bestPlacement = placement
                low = mid + 1
            } else {
                high = mid - 1
            }
        }

        return bestPlacement
    }

    private fun validatePlacement(
        placement: PlacementCandidate,
        constraints: BLS
    ): PlacementValidation {
        val candidateDurationSeconds = Duration.between(
            placement.adjustedCandidate.time,
            placement.adjustedCandidate.timeStop
        ).seconds.toDouble()
        if (candidateDurationSeconds < constraints.durationMin) {
            return PlacementValidation(false, "duration_min")
        }

        if (placement.mergedSurvey.duration > constraints.durationMax) {
            return PlacementValidation(false, "duration_max")
        }

        if (hasDifferentRollConflict(placement, constraints)) {
            return PlacementValidation(false, "different_roll_conflict")
        }

        if (exceedsDailyMaxDuration(placement.resultingSurveys, constraints)) {
            return PlacementValidation(false, "daily_max_duration")
        }

        if (exceedsRevolutionMaxDuration(placement.resultingSurveys, constraints)) {
            return PlacementValidation(false, "revolution_max_duration")
        }

        return PlacementValidation(true)
    }

    private fun applyPlacement(
        existingSurveys: MutableList<SurveyMode>,
        placement: PlacementCandidate
    ) {
        existingSurveys.removeAll(placement.absorbedSurveys.toSet())
        existingSurveys.add(placement.mergedSurvey)
        existingSurveys.sortBy { it.time }
    }

    private data class MergeChain(
        val absorbedSurveys: List<SurveyMode>,
        val mergedSurvey: SurveyMode
    )

    private fun buildMergeChain(
        existingSurveys: List<SurveyMode>,
        candidateSurvey: SurveyMode,
        satelliteId: Long
    ): MergeChain {
        val absorbed = linkedSetOf<SurveyMode>()
        var mergedSurvey = candidateSurvey
        var expanded = true

        while (expanded) {
            expanded = false
            existingSurveys.forEach { existing ->
                if (absorbed.contains(existing)) {
                    return@forEach
                }
                if (!sameRoll(existing.roll, candidateSurvey.roll)) {
                    return@forEach
                }
                if (!canMergeInTime(existing.time, existing.timeStop, mergedSurvey.time, mergedSurvey.timeStop)) {
                    return@forEach
                }
                if (shouldSkipComplanInsideSlotsMerge(existing, mergedSurvey)) {
                    return@forEach
                }
                val overlapsInTime = intersectsInTime(
                    existing.time,
                    existing.timeStop,
                    mergedSurvey.time,
                    mergedSurvey.timeStop
                )
                val geometryMergeAssessment = assessMergeableGeometry(
                    leftContour = existing.contourWKT,
                    rightContour = mergedSurvey.contourWKT,
                    satelliteId = satelliteId,
                    allowContinuityWithoutArea = !overlapsInTime
                )
                if (!geometryMergeAssessment.mergeable) {
                    if (geometryMergeAssessment.geometryChecked) {
                        logMergeSkippedWithoutSufficientGeometry(
                            satelliteId = satelliteId,
                            candidateSurvey = mergedSurvey,
                            existingSurvey = existing,
                            intersectionArea = geometryMergeAssessment.intersectionArea ?: 0.0,
                            distance = geometryMergeAssessment.distance,
                            overlapsInTime = overlapsInTime
                        )
                    }
                    return@forEach
                }
                val mergedContour = unionContoursIfPolygon(
                    baseContour = mergedSurvey.contourWKT,
                    contourToAdd = existing.contourWKT,
                    satelliteId = satelliteId,
                    candidateSurvey = mergedSurvey,
                    existingSurvey = existing
                ) ?: return@forEach
                absorbed += existing
                mergedSurvey = mergeSurveys(mergedSurvey, existing, mergedContour)
                expanded = true
            }
        }

        return MergeChain(absorbed.toList(), mergedSurvey)
    }

    private fun mergeSurveys(
        baseSurvey: SurveyMode,
        existingSurvey: SurveyMode,
        mergedContour: String
    ): SurveyMode {
        val mergedStart = minOf(baseSurvey.time, existingSurvey.time)
        val mergedStop = maxOf(baseSurvey.timeStop, existingSurvey.timeStop)
        val mergedRevolution = if (existingSurvey.time < baseSurvey.time) {
            existingSurvey.revolution
        } else {
            baseSurvey.revolution
        }
        val mergedBookedSlotIds = (baseSurvey.bookedSlotIds + existingSurvey.bookedSlotIds).distinct()
        val mergedCellNums = normalizeCellNums(baseSurvey).apply {
            addAll(normalizeCellNums(existingSurvey))
        }
        return SurveyMode(
            revolution = mergedRevolution,
            time = mergedStart,
            timeStop = mergedStop,
            roll = baseSurvey.roll,
            latitude = baseSurvey.latitude,
            longitude = baseSurvey.longitude,
            duration = Duration.between(mergedStart, mergedStop).seconds.toDouble(),
            contourWKT = mergedContour,
            cellNum = mergedCellNums.singleOrNull() ?: -1,
            source = mergeSources(baseSurvey.source, existingSurvey.source),
            bookedSlotIds = mergedBookedSlotIds,
            cellNums = mergedCellNums.toList().sorted()
        )
    }

    private fun hasDifferentRollConflict(
        placement: PlacementCandidate,
        constraints: BLS
    ): Boolean =
        placement.resultingSurveys.any { existing ->
            existing !== placement.mergedSurvey &&
                !sameRoll(existing.roll, placement.mergedSurvey.roll) &&
                intersectsWithMmi(
                    existing.time,
                    existing.timeStop,
                    placement.mergedSurvey.time,
                    placement.mergedSurvey.timeStop,
                    constraints.mmi
                )
        }

    private fun exceedsDailyMaxDuration(
        surveys: List<SurveyMode>,
        constraints: BLS
    ): Boolean =
        surveys
            .flatMap { splitSurveyDurationByDay(it) }
            .groupBy({ it.first }, { it.second })
            .values
            .any { durations -> durations.sum() > constraints.dailyMaxDuration }

    private fun exceedsRevolutionMaxDuration(
        surveys: List<SurveyMode>,
        constraints: BLS
    ): Boolean =
        surveys
            .groupBy { it.revolution }
            .values
            .any { grouped -> grouped.sumOf { it.duration } > constraints.revolutionMaxDuration }

    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).seconds.toDouble()
            current = nextDayStart
        }

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

    private fun trimCandidate(candidateSurvey: SurveyMode, allowedDurationSeconds: Long): SurveyMode {
        val trimmedEnd = candidateSurvey.time.plusSeconds(allowedDurationSeconds.coerceAtLeast(0))
        return candidateSurvey.copyWith(
            timeStop = trimmedEnd,
            duration = Duration.between(candidateSurvey.time, trimmedEnd).seconds.toDouble()
        )
    }

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

    private fun canMergeInTime(
        start1: LocalDateTime,
        stop1: LocalDateTime,
        start2: LocalDateTime,
        stop2: LocalDateTime
    ): Boolean {
        val firstStop = minOf(stop1, stop2)
        val secondStart = maxOf(start1, start2)
        if (!secondStart.isAfter(firstStop)) {
            return true
        }
        val gapSeconds = Duration.between(firstStop, secondStart).seconds
        return gapSeconds in 0..MERGE_MAX_TIME_GAP_SECONDS
    }

    private fun shouldSkipComplanInsideSlotsMerge(
        left: SurveyMode,
        right: SurveyMode
    ): Boolean {
        val complanSurvey = when {
            left.source == SatelliteModeSource.COMPLAN && right.source == SatelliteModeSource.SLOTS -> left
            right.source == SatelliteModeSource.COMPLAN && left.source == SatelliteModeSource.SLOTS -> right
            else -> return false
        }
        val slotsSurvey = if (complanSurvey === left) right else left

        return !complanSurvey.time.isBefore(slotsSurvey.time) &&
            !complanSurvey.timeStop.isAfter(slotsSurvey.timeStop)
    }

    private fun intersectsInTime(
        start1: LocalDateTime,
        stop1: LocalDateTime,
        start2: LocalDateTime,
        stop2: LocalDateTime
    ): Boolean = start1 < stop2 && start2 < stop1

    private fun intersectsWithMmi(
        start1: LocalDateTime,
        stop1: LocalDateTime,
        start2: LocalDateTime,
        stop2: LocalDateTime,
        mmiSeconds: Double
    ): Boolean {
        val mmi = ceil(mmiSeconds).toLong()
        return start1 <= stop2.plusSeconds(mmi) && start2 <= stop1.plusSeconds(mmi)
    }

    private fun mergeSources(left: SatelliteModeSource, right: SatelliteModeSource): SatelliteModeSource = when {
        left == right -> left
        left == SatelliteModeSource.MIXED || right == SatelliteModeSource.MIXED -> SatelliteModeSource.MIXED
        else -> SatelliteModeSource.MIXED
    }

    private fun normalizeCellNums(survey: SurveyMode): MutableSet<Long> =
        survey.cellNums
            .ifEmpty { if (survey.cellNum > 0) listOf(survey.cellNum) else emptyList() }
            .filter { it > 0 }
            .toMutableSet()

    private fun assessMergeableGeometry(
        leftContour: String,
        rightContour: String,
        satelliteId: Long,
        allowContinuityWithoutArea: Boolean
    ): GeometryMergeAssessment {
        return try {
            val reader = WKTReader()
            val leftGeometry = reader.read(leftContour)
            val rightGeometry = reader.read(rightContour)
            logInvalidGeometryIfNeeded(satelliteId, "left", leftGeometry)
            logInvalidGeometryIfNeeded(satelliteId, "right", rightGeometry)
            val intersectionArea = leftGeometry.intersection(rightGeometry).area
            val touchesOrIntersects = leftGeometry.intersects(rightGeometry) || leftGeometry.touches(rightGeometry)
            val distance = if (touchesOrIntersects) 0.0 else leftGeometry.distance(rightGeometry)
            GeometryMergeAssessment(
                mergeable = intersectionArea > GEOMETRY_INTERSECTION_AREA_EPSILON ||
                    (allowContinuityWithoutArea && (touchesOrIntersects || distance <= GEOMETRY_DISTANCE_EPSILON)),
                intersectionArea = intersectionArea,
                touchesOrIntersects = touchesOrIntersects,
                distance = distance
            )
        } catch (e: Exception) {
            logger.warn("Не удалось проверить непрерывность геометрий маршрутов при placement: {}", e.message)
            GeometryMergeAssessment(mergeable = false, geometryChecked = false)
        }
    }

    private fun unionContoursIfPolygon(
        baseContour: String,
        contourToAdd: String,
        satelliteId: Long,
        candidateSurvey: SurveyMode,
        existingSurvey: SurveyMode
    ): String? {
        return try {
            val reader = WKTReader()
            val unionedGeometry = reader.read(baseContour).union(reader.read(contourToAdd))
            if (unionedGeometry !is Polygon) {
                logger.warn(
                    "Merge skipped because union produced non-polygon geometry: satelliteId={}, geometryType={}, candidateStart={}, candidateEnd={}, existingStart={}, existingEnd={}, candidateRoll={}, existingRoll={}, candidateSource={}, existingSource={}",
                    satelliteId,
                    unionedGeometry.geometryType,
                    candidateSurvey.time,
                    candidateSurvey.timeStop,
                    existingSurvey.time,
                    existingSurvey.timeStop,
                    candidateSurvey.roll,
                    existingSurvey.roll,
                    candidateSurvey.source,
                    existingSurvey.source
                )
                return null
            }
            WKTWriter().write(unionedGeometry)
        } catch (e: Exception) {
            logger.warn("Не удалось объединить контуры маршрутов при placement: {}", e.message)
            null
        }
    }

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

    private fun logMergeSkippedWithoutSufficientGeometry(
        satelliteId: Long,
        candidateSurvey: SurveyMode,
        existingSurvey: SurveyMode,
        intersectionArea: Double,
        distance: Double?,
        overlapsInTime: Boolean
    ) {
        if (overlapsInTime) {
            logger.warn(
                "Merge skipped because contours lack sufficient areal intersection despite temporal overlap: satelliteId={}, intersectionArea={}, epsilon={}, candidateStart={}, candidateEnd={}, existingStart={}, existingEnd={}, candidateRoll={}, existingRoll={}, candidateSource={}, existingSource={}, candidateCellNums={}, existingCellNums={}, candidateBookedSlotIds={}, existingBookedSlotIds={}",
                satelliteId,
                intersectionArea,
                GEOMETRY_INTERSECTION_AREA_EPSILON,
                candidateSurvey.time,
                candidateSurvey.timeStop,
                existingSurvey.time,
                existingSurvey.timeStop,
                candidateSurvey.roll,
                existingSurvey.roll,
                candidateSurvey.source,
                existingSurvey.source,
                normalizeCellNums(candidateSurvey).toList().sorted(),
                normalizeCellNums(existingSurvey).toList().sorted(),
                candidateSurvey.bookedSlotIds,
                existingSurvey.bookedSlotIds
            )
            return
        }

        logger.warn(
            "Merge skipped because contours are not continuous enough despite mergeable time window: satelliteId={}, intersectionArea={}, areaEpsilon={}, distance={}, distanceEpsilon={}, maxTimeGapSeconds={}, candidateStart={}, candidateEnd={}, existingStart={}, existingEnd={}, candidateRoll={}, existingRoll={}, candidateSource={}, existingSource={}, candidateCellNums={}, existingCellNums={}, candidateBookedSlotIds={}, existingBookedSlotIds={}",
            satelliteId,
            intersectionArea,
            GEOMETRY_INTERSECTION_AREA_EPSILON,
            distance,
            GEOMETRY_DISTANCE_EPSILON,
            MERGE_MAX_TIME_GAP_SECONDS,
            candidateSurvey.time,
            candidateSurvey.timeStop,
            existingSurvey.time,
            existingSurvey.timeStop,
            candidateSurvey.roll,
            existingSurvey.roll,
            candidateSurvey.source,
            existingSurvey.source,
            normalizeCellNums(candidateSurvey).toList().sorted(),
            normalizeCellNums(existingSurvey).toList().sorted(),
            candidateSurvey.bookedSlotIds,
            existingSurvey.bookedSlotIds
        )
    }

    private fun SurveyMode.copyWith(
        time: LocalDateTime = this.time,
        timeStop: LocalDateTime = this.timeStop,
        duration: Double = this.duration,
        revolution: Long = this.revolution,
        contourWKT: String = this.contourWKT,
        cellNum: Long = this.cellNum,
        source: SatelliteModeSource = this.source,
        bookedSlotIds: List<Long> = this.bookedSlotIds,
        cellNums: List<Long> = this.cellNums
    ) = SurveyMode(
        revolution = revolution,
        time = time,
        timeStop = timeStop,
        roll = roll,
        latitude = latitude,
        longitude = longitude,
        duration = duration,
        contourWKT = contourWKT,
        cellNum = cellNum,
        source = source,
        bookedSlotIds = bookedSlotIds,
        cellNums = cellNums
    )
}
