package space.nstart.pcp.pcp_coverage_scheme_service.service

import ballistics.flightLine.PointOnEarthCalculator
import ballistics.types.EarthType
import ballistics.types.Orientation
import ballistics.types.OrbitalPoint
import ballistics.types.THBLPoint
import ballistics.types.WorkCSType
import ballistics.utils.fromDateTime
import ballistics.utils.math.Vector3D
import org.locationtech.jts.geom.Coordinate
import org.locationtech.jts.geom.Geometry
import org.locationtech.jts.geom.GeometryFactory
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import space.nstart.pcp.pcp_coverage_scheme_service.client.CoverageBallisticsClient
import space.nstart.pcp.pcp_coverage_scheme_service.client.CoverageSatelliteClient
import space.nstart.pcp.pcp_types_lib.dto.ballistics.ExactTimePositionRequestDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.ObjDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.ObjViewRequestDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.PositionDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign
import space.nstart.pcp.pcp_types_lib.dto.coverage.CoverageObservationDTO
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SurveySlotDTO
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteInfoDTO
import java.time.Duration
import java.time.LocalDateTime
import kotlin.math.PI
import kotlin.math.max
import kotlin.math.min

@Service
class CoverageSchemeBuilderService(
    private val ballisticsClient: CoverageBallisticsClient,
    private val satelliteClient: CoverageSatelliteClient,
    private val geometrySupport: GeometrySupport
) {

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

    fun buildCoverageScheme(
        observations: List<CoverageObservationDTO>,
        rollStepDegrees: Double? = null,
        longitudeModel: GeometrySupport.LongitudeModel? = null
    ): List<SurveySlotDTO> {
        val satelliteParametersBySatellite = HashMap<Long, SatelliteSurveyParameters>()
        val selectedBranch = selectDominantBranch(observations)
        val selectedObservations = observations.filter { it.observation.revSignBegin == selectedBranch }

        logger.info(
            "Selected observation branch for coverage scheme: branch={}, ascCount={}, descCount={}, selectedCount={}",
            selectedBranch,
            observations.count { it.observation.revSignBegin == space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign.ASC },
            observations.count { it.observation.revSignBegin == space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign.DESC },
            selectedObservations.size
        )

        val preparedObservations = selectedObservations.flatMap { observation ->
            prepareObservation(observation, satelliteParametersBySatellite, longitudeModel)
        }.sortedWith(
            compareBy<PreparedObservation>(
                { it.leftLongitude },
                { if (selectedBranch == RevolutionSign.DESC) it.observation.observation.gammaMax else 0.0},
                { it.segmentTimeBegin },
                { it.segmentTimeEnd },
                { it.observation.observation.noradId }
            )
        )

        if (preparedObservations.isEmpty()) {
            return emptyList()
        }

        val selectedContours = ArrayList<SelectedContour>(preparedObservations.size)
        var previousContour: SelectedContour? = null
        var requiredRollSign: Int? = null
        var coveredTargetArea: Geometry = geometryFactory.createPolygon()

        preparedObservations.forEach { observation ->
            val selectedContour = selectContour(observation, previousContour, requiredRollSign) ?: return@forEach
            if (!shouldAddContour(selectedContour, coveredTargetArea)) {
                return@forEach
            }
            selectedContours += selectedContour
            previousContour = selectedContour
            coveredTargetArea = if (coveredTargetArea.isEmpty) {
                selectedContour.targetCoverage
            } else {
                geometrySupport.safeUnion(coveredTargetArea, selectedContour.targetCoverage)
            }
            if (requiredRollSign == null) {
                requiredRollSign = rollSign(selectedContour.roll)
            }
        }

        return selectedContours.map { selectedContour ->
            val observation = selectedContour.observation.observation.observation
            val viewPoint = viewParams(
                PointOnEarthCalculator(EarthType.PZ90d02, WorkCSType.WCSOrbit),
                selectedContour.observation.orbitalPoints.first().toOrbitalPoint(),
                selectedContour.roll
            )
            SurveySlotDTO(
                satelliteId = observation.noradId,
                tn = selectedContour.observation.segmentTimeBegin,
                tk = selectedContour.observation.segmentTimeEnd,
                roll = selectedContour.roll,
                contour = geometrySupport.toWkt(selectedContour.contour),
                revolution = observation.revolutionBegin,
                revolutionSign = observation.revSignBegin,
                latitude = viewPoint?.lat?.toDegrees() ?: 0.0,
                longitude = normalizeLongitude(
                    viewPoint?.long?.toDegrees() ?: 0.0,
                    selectedContour.observation.longitudeModel
                )
            )
        }
    }

    private fun prepareObservation(
        observation: CoverageObservationDTO,
        satelliteParametersBySatellite: MutableMap<Long, SatelliteSurveyParameters>,
        longitudeModel: GeometrySupport.LongitudeModel?
    ): List<PreparedObservation> {
        val resolvedLongitudeModel = longitudeModel
            ?: observation.intersectionContourWkts
                .firstOrNull { it.isNotBlank() }
                ?.let(geometrySupport::resolveLongitudeModel)
            ?: GeometrySupport.LongitudeModel.POSITIVE_360
        val intersectionContour = observation.intersectionContourWkts
            .filter { it.isNotBlank() }
            .map { geometrySupport.parseAndNormalizeTarget(it, resolvedLongitudeModel) }
            .reduceOrNull(geometrySupport::safeUnion)

        if (intersectionContour == null || intersectionContour.isEmpty) {
            logger.debug(
                "Skip coverage scheme slot because target intersection contour is empty: satelliteId={}, interval=[{} - {}]",
                observation.observation.noradId,
                observation.observation.timeBegin,
                observation.observation.timeEnd
            )
            return emptyList()
        }

        val satelliteParameters = satelliteParametersBySatellite.getOrPut(observation.observation.noradId) {
            satelliteClient.satellite(observation.observation.noradId).toSurveyParameters()
        }

        return splitObservationIntervals(observation, satelliteParameters)
            .mapNotNull { segment ->
                val orbitalPoints = ballisticsClient.exactTime(
                    observation.observation.noradId,
                    ExactTimePositionRequestDTO(
                        time = segment.timeBegin,
                        timeStop = segment.timeEnd,
                        stepMs = contourStepMs(segment.timeBegin, segment.timeEnd)
                    )
                ).sortedBy { it.time }

                if (orbitalPoints.isEmpty()) {
                    logger.debug(
                        "Skip coverage scheme slot because orbital points are empty: satelliteId={}, interval=[{} - {}]",
                        observation.observation.noradId,
                        segment.timeBegin,
                        segment.timeEnd
                    )
                    return@mapNotNull null
                }

                PreparedObservation(
                    observation = observation,
                    intersectionContour = intersectionContour,
                    orbitalPoints = orbitalPoints,
                    capture = satelliteParameters.captureAngle,
                    leftLongitude = intersectionContour.envelopeInternal.minX,
                    longitudeModel = resolvedLongitudeModel,
                    segmentTimeBegin = segment.timeBegin,
                    segmentTimeEnd = segment.timeEnd
                )
            }
    }

    private fun selectContour(
        observation: PreparedObservation,
        previousContour: SelectedContour?,
        requiredRollSign: Int?
    ): SelectedContour? {
        val branch = observation.observation.observation.revSignBegin
        val defaultRoll = resolveCompatibleRoll(
            candidate = defaultRoll(branch, observation),
            branch = branch,
            observation = observation,
            requiredRollSign = requiredRollSign
        ) ?: run {
            logger.warn(
                "Skip coverage scheme slot because interval does not support required roll sign: satelliteId={}, interval=[{} - {}], requiredSign={}",
                observation.observation.observation.noradId,
                observation.observation.observation.timeBegin,
                observation.observation.observation.timeEnd,
                requiredRollSign
            )
            return null
        }
        val defaultContour = buildContour(observation, defaultRoll) ?: return null

        if (previousContour == null) {
            return defaultContour
        }

        if (!intersects(observation.intersectionContour, previousContour.contour)) {
            return defaultContour
        }

        val rollFromIntersection = resolveIntersectingRoll(observation, previousContour)
            ?.let { candidate ->
                resolveCompatibleRoll(
                    candidate = candidate,
                    branch = branch,
                    observation = observation,
                    requiredRollSign = requiredRollSign
                )
            }
            ?: return defaultContour
        return buildContour(observation, rollFromIntersection) ?: defaultContour
    }

    private fun buildContour(observation: PreparedObservation, roll: Double): SelectedContour? {
        val contour = contour(observation.orbitalPoints, roll, observation.capture, observation.intersectionContour) ?: return null
        if (contour.isEmpty) {
            return null
        }
        val targetCoverage = geometrySupport.safeIntersection(contour, observation.intersectionContour)
        return SelectedContour(
            observation = observation,
            roll = roll,
            contour = contour,
            targetCoverage = targetCoverage,
            overlapAnchor = overlapAnchorPoint(
                branch = observation.observation.observation.revSignBegin,
                points = observation.orbitalPoints,
                roll = roll,
                capture = observation.capture,
                longitudeModel = observation.longitudeModel
            ) ?: contour.coordinates.firstOrNull()
        )
    }

    private fun contour(points: List<OrbPointDTO>, roll: Double, capture: Double, target: Geometry): Geometry? {
        return try {
            val pointOnEarthCalculator = PointOnEarthCalculator(EarthType.PZ90d02, WorkCSType.WCSOrbit)
            val rightCoordinates = ArrayList<Coordinate>(points.size)
            val leftCoordinates = ArrayList<Coordinate>(points.size)

            points.map { it.toOrbitalPoint() }.forEach { point ->
                val rightPoint = viewParams(pointOnEarthCalculator, point, roll + capture) ?: THBLPoint(0.0, 0.0, 0.0, 0.0)
                rightCoordinates += Coordinate(rightPoint.long.toDegrees(), rightPoint.lat.toDegrees(), 0.0)

                val leftPoint = viewParams(pointOnEarthCalculator, point, roll - capture) ?: THBLPoint(0.0, 0.0, 0.0, 0.0)
                leftCoordinates += Coordinate(leftPoint.long.toDegrees(), leftPoint.lat.toDegrees(), 0.0)
            }

            val alignedRightCoordinates = geometrySupport.alignTrackCoordinatesToTarget(rightCoordinates, target)
            val alignedLeftCoordinates = geometrySupport.alignTrackCoordinatesToTarget(leftCoordinates, target)
            val shellCoordinates = ArrayList<Coordinate>(rightCoordinates.size + leftCoordinates.size + 1)
            shellCoordinates.addAll(alignedRightCoordinates)
            for (index in alignedLeftCoordinates.indices.reversed()) {
                shellCoordinates += alignedLeftCoordinates[index]
            }
            if (shellCoordinates.isNotEmpty()) {
                shellCoordinates += Coordinate(shellCoordinates.first().x, shellCoordinates.first().y, 0.0)
            }
            if (shellCoordinates.size < 4) {
                return null
            }

            val shell = geometryFactory.createLinearRing(shellCoordinates.toTypedArray())
            geometryFactory.createPolygon(shell).buffer(0.0)
        } catch (error: Exception) {
            logger.warn("Failed to build coverage scheme contour", error)
            null
        }
    }

    private fun viewParams(
        pointOnEarthCalculator: PointOnEarthCalculator,
        point: OrbitalPoint,
        gamma: Double
    ): THBLPoint? =
        pointOnEarthCalculator.pointOnEarth(point, Orientation(0.0, gamma * PI / 180.0, 0.0))

    private fun selectDominantBranch(observations: List<CoverageObservationDTO>) =
        if (observations.count { it.observation.revSignBegin == space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign.ASC } >=
            observations.count { it.observation.revSignBegin == space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign.DESC }
        ) {
            space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign.ASC
        } else {
            space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign.DESC
        }

    private fun intersects(first: Geometry, second: Geometry): Boolean =
        !geometrySupport.safeIntersection(first, second).isEmpty

    private fun defaultRoll(branch: RevolutionSign, observation: PreparedObservation): Double =
        when (branch) {
            RevolutionSign.ASC -> max(observation.observation.observation.gammaMin, observation.observation.observation.gammaMax)
            RevolutionSign.DESC -> min(observation.observation.observation.gammaMin, observation.observation.observation.gammaMax)
        }

    private fun resolveCompatibleRoll(
        candidate: Double,
        branch: RevolutionSign,
        observation: PreparedObservation,
        requiredRollSign: Int?
    ): Double? {
        if (requiredRollSign == null || rollSign(candidate) == requiredRollSign) {
            return candidate
        }

        val gammaMin = min(observation.observation.observation.gammaMin, observation.observation.observation.gammaMax)
        val gammaMax = max(observation.observation.observation.gammaMin, observation.observation.observation.gammaMax)

        return when (requiredRollSign) {
            1 -> when {
                gammaMin > 0.0 && branch == RevolutionSign.DESC -> gammaMin
                gammaMax > 0.0 -> gammaMax
                else -> null
            }
            -1 -> when {
                gammaMax < 0.0 && branch == RevolutionSign.ASC -> gammaMax
                gammaMin < 0.0 -> gammaMin
                else -> null
            }
            else -> if (gammaMin <= 0.0 && gammaMax >= 0.0) 0.0 else null
        }
    }

    private fun resolveIntersectingRoll(
        observation: PreparedObservation,
        previousContour: SelectedContour
    ): Double? {
        val previousContourCoordinates = previousContour.contour.coordinates
        if (previousContourCoordinates.size < 4) {
            logger.warn(
                "Unable to resolve intersection roll because previous contour geometry is too short: previousSatelliteId={}, currentSatelliteId={}, points={}",
                previousContour.observation.observation.observation.noradId,
                observation.observation.observation.noradId,
                previousContourCoordinates.size
            )
            return null
        }

        val rightmostStartPoint = listOf(
            previousContourCoordinates.first(),
            previousContourCoordinates[previousContourCoordinates.size - 2]
        ).maxByOrNull { it.x } ?: return null

        val mplPoints = ballisticsClient.mplPoint(
            ObjViewRequestDTO(
                satellites = listOf(observation.observation.observation.noradId),
                timeStart = observation.segmentTimeBegin.minusSeconds(200),
                timeStop = observation.segmentTimeEnd.plusSeconds(200),
                obj = ObjDTO(
                    id = "coverage-scheme-overlap",
                    position = PositionDTO(
                        lat = rightmostStartPoint.y,
                        long = normalizeLongitude(rightmostStartPoint.x, observation.longitudeModel),
                        height = 0.0
                    )
                )
            )
        )

        if (mplPoints.size != 1) {
            logger.warn(
                "Intersection roll lookup returned unexpected mplPoint count, fallback to no-intersection behavior: currentSatelliteId={}, interval=[{} - {}], mplPointCount={}",
                observation.observation.observation.noradId,
                observation.segmentTimeBegin,
                observation.segmentTimeEnd,
                mplPoints.size
            )
            return null
        }

        val mplGamma = mplPoints.single().gamma
        val currentPoint = observation.orbitalPoints.firstOrNull()?.toOrbitalPoint() ?: return null
        val pointOnEarthCalculator = PointOnEarthCalculator(EarthType.PZ90d02, WorkCSType.WCSOrbit)
        val captureHalf = observation.capture
        val gammaCandidates = listOf(mplGamma - captureHalf, mplGamma + captureHalf)
        val longitudeReference = rightmostStartPoint.x

        return gammaCandidates
            .mapNotNull { gamma ->
                pointOnEarthCalculator.pointOnEarth(
                    currentPoint,
                    Orientation(0.0, gamma * PI / 180.0, 0.0)
                )?.let { pointOnEarth ->
                    var adjustedLongitude = pointOnEarth.long.toDegrees()
                    while (adjustedLongitude - longitudeReference > 180.0) adjustedLongitude -= 360.0
                    while (adjustedLongitude - longitudeReference < -180.0) adjustedLongitude += 360.0
                    gamma to adjustedLongitude
                }
            }
            .maxByOrNull { (_, longitude) -> longitude }
            ?.first
    }

    private fun shouldAddContour(candidate: SelectedContour, coveredTargetArea: Geometry): Boolean {
        if (candidate.targetCoverage.isEmpty || candidate.targetCoverage.area <= 0.0) {
            logger.info(
                "Skip coverage scheme slot because candidate does not intersect required target area: satelliteId={}, interval=[{} - {}]",
                candidate.observation.observation.observation.noradId,
                candidate.observation.segmentTimeBegin,
                candidate.observation.segmentTimeEnd
            )
            return false
        }

        if (coveredTargetArea.isEmpty) {
            return true
        }

        val overlapArea = geometrySupport.safeIntersection(candidate.targetCoverage, coveredTargetArea).area
        val overlapRatio = overlapArea / candidate.targetCoverage.area
        if (overlapRatio > 0.9) {
            logger.info(
                "Skip coverage scheme slot because candidate overlaps existing coverage too much: satelliteId={}, interval=[{} - {}], overlapRatio={}",
                candidate.observation.observation.observation.noradId,
                candidate.observation.segmentTimeBegin,
                candidate.observation.segmentTimeEnd,
                overlapRatio
            )
            return false
        }

        return true
    }

    private fun overlapAnchorPoint(
        branch: RevolutionSign,
        points: List<OrbPointDTO>,
        roll: Double,
        capture: Double,
        longitudeModel: GeometrySupport.LongitudeModel
    ): Coordinate? {
        val firstPoint = points.firstOrNull()?.toOrbitalPoint() ?: return null
        val pointOnEarthCalculator = PointOnEarthCalculator(EarthType.PZ90d02, WorkCSType.WCSOrbit)
        val boundaryPoint = when (branch) {
            RevolutionSign.ASC -> viewParams(pointOnEarthCalculator, firstPoint, roll - capture)
            RevolutionSign.DESC -> viewParams(pointOnEarthCalculator, firstPoint, roll + capture)
        } ?: return null
        return Coordinate(
            normalizeLongitude(boundaryPoint.long.toDegrees(), longitudeModel),
            boundaryPoint.lat.toDegrees(),
            0.0
        )
    }

    private fun splitObservationIntervals(
        observation: CoverageObservationDTO,
        satelliteParameters: SatelliteSurveyParameters
    ): List<ObservationSegment> {
        val maxDurationSeconds = satelliteParameters.maxSurveyDurationSeconds.coerceAtLeast(1L)
        val mmiSeconds = satelliteParameters.mmiSeconds.coerceAtLeast(0L)
        val segments = mutableListOf<ObservationSegment>()
        var segmentStart = observation.observation.timeBegin

        while (segmentStart.isBefore(observation.observation.timeEnd)) {
            val segmentEnd = minOf(
                segmentStart.plusSeconds(maxDurationSeconds),
                observation.observation.timeEnd
            )
            if (!segmentEnd.isAfter(segmentStart)) {
                break
            }
            segments += ObservationSegment(segmentStart, segmentEnd)
            if (!segmentEnd.isBefore(observation.observation.timeEnd)) {
                break
            }
            segmentStart = segmentEnd.plusSeconds(mmiSeconds)
        }

        return segments
    }

    private fun contourStepMs(timeBegin: LocalDateTime, timeEnd: LocalDateTime): Long {
        val durationMs = max(1L, Duration.between(timeBegin, timeEnd).toMillis())
        return max(1L, durationMs / 5L)
    }

    private fun rollSign(roll: Double): Int = when {
        roll > 0.0 -> 1
        roll < 0.0 -> -1
        else -> 0
    }

    private fun normalizeLongitude(longitude: Double, longitudeModel: GeometrySupport.LongitudeModel): Double =
        when (longitudeModel) {
            GeometrySupport.LongitudeModel.STANDARD_180 -> {
                var normalized = longitude
                while (normalized > 180.0) normalized -= 360.0
                while (normalized <= -180.0) normalized += 360.0
                normalized
            }
            GeometrySupport.LongitudeModel.POSITIVE_360 -> {
                var normalized = longitude
                while (normalized < 0.0) normalized += 360.0
                while (normalized >= 360.0) normalized -= 360.0
                normalized
            }
        }

    private fun OrbPointDTO.toOrbitalPoint() = OrbitalPoint(
        t = fromDateTime(time),
        vit = revolution.toInt(),
        r = Vector3D(x, y, z),
        v = Vector3D(vx, vy, vz)
    )

    private fun Double.toDegrees(): Double = this * 180.0 / PI

    private data class PreparedObservation(
        val observation: CoverageObservationDTO,
        val intersectionContour: Geometry,
        val orbitalPoints: List<OrbPointDTO>,
        val capture: Double,
        val leftLongitude: Double,
        val longitudeModel: GeometrySupport.LongitudeModel,
        val segmentTimeBegin: LocalDateTime,
        val segmentTimeEnd: LocalDateTime
    )

    private data class SelectedContour(
        val observation: PreparedObservation,
        val roll: Double,
        val contour: Geometry,
        val targetCoverage: Geometry,
        val overlapAnchor: Coordinate?
    )

    private data class SatelliteSurveyParameters(
        val captureAngle: Double,
        val maxSurveyDurationSeconds: Long,
        val mmiSeconds: Long
    )

    private data class ObservationSegment(
        val timeBegin: LocalDateTime,
        val timeEnd: LocalDateTime
    )

    private fun SatelliteInfoDTO.toSurveyParameters() = SatelliteSurveyParameters(
        captureAngle = captureAngle,
        maxSurveyDurationSeconds = maxSurveyDurationSeconds ?: 300L,
        mmiSeconds = mmiSeconds ?: 10L
    )
}
