package space.nstart.pcp.pcp_request_service.angular

import ballistics.orbitalPoints.timeStepper.RungeStepper
import ballistics.types.EarthType
import ballistics.types.OrbitalPoint
import ballistics.utils.fromDateTime
import ballistics.utils.toDateTime
import ballistics.utils.math.Vector3D
import org.springframework.stereotype.Service
import space.nstart.pcp.angularmotion.AngularMotionCalculatorFactory
import space.nstart.pcp.angularmotion.AngularMotionConfig
import space.nstart.pcp.angularmotion.AngularMotionMode
import space.nstart.pcp.angularmotion.SurveyContourCalculator
import space.nstart.pcp.angularmotion.SurveyId
import space.nstart.pcp.pcp_request_service.configuration.CustomValidationException
import space.nstart.pcp.pcp_request_service.service.SatelliteCatalogClient
import space.nstart.pcp.pcp_request_service.service.SatelliteService
import space.nstart.pcp.pcp_types_lib.dto.ballistics.AngularMotionCalculationRequestDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.AngularMotionModeDTO
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 kotlin.math.PI
import kotlin.math.abs
import kotlin.math.atan
import kotlin.math.max
import kotlin.math.roundToLong

@Service
class AngularMotionService(
    private val satelliteService: SatelliteService,
    private val satelliteCatalogClient: SatelliteCatalogClient,
) {
    fun availableSatellites(time: java.time.LocalDateTime?): List<AngularMotionSatelliteDTO> {
        val satellitesByBallisticsId = satelliteCatalogClient.getSatellites()
            .flatMap { satellite ->
                listOfNotNull(
                    satellite.id to satellite,
                    satellite.noradId?.let { it to satellite },
                )
            }
            .toMap()
        val availability = time
            ?.let { satelliteService.getOrbitAvailabilityForDate(it) }
            ?: satelliteService.getOrbitAvailability(null)

        return availability
            .map { availability ->
                val satellite = satellitesByBallisticsId[availability.satelliteId]
                AngularMotionSatelliteDTO(
                    satelliteId = availability.satelliteId,
                    code = satellite?.code ?: availability.satelliteId.toString(),
                    name = satellite?.name ?: "КА ${availability.satelliteId}",
                    typeCode = satellite?.typeCode.orEmpty(),
                    availabilityStart = availability.timeStart,
                    availabilityStop = availability.timeStop,
                )
            }
            .sortedWith(compareBy<AngularMotionSatelliteDTO> { it.code }.thenBy { it.satelliteId })
    }

    fun flightLine(satelliteId: Long, time: java.time.LocalDateTime): List<AngularMotionFlightLinePointDTO> {
        val start = time.minusMinutes(MAP_INTERVAL_MINUTES)
        val stop = time.plusMinutes(MAP_INTERVAL_MINUTES)
        return satelliteService.getFL(satelliteId, start, stop, 60.0).map { point ->
            AngularMotionFlightLinePointDTO(
                time = point.time,
                revolution = point.revolution,
                latitudeDeg = point.lat,
                longitudeDeg = point.long,
                leftLatitudeDeg = point.latLeft,
                leftLongitudeDeg = point.longLeft,
                rightLatitudeDeg = point.latRight,
                rightLongitudeDeg = point.longRight,
            )
        }
    }

    fun calculate(request: AngularMotionCalculationRequestDTO): AngularMotionCalculationResultDTO {
        validate(request)
        validateTargetVisibility(request)
        val defaultConfig = AngularMotionConfig()
        val config = AngularMotionConfig(
            focus = request.focusMm ?: defaultConfig.focus,
            stepPuud = request.stepPuudSec ?: defaultConfig.stepPuud,
        )
        val sourcePoints = loadOrbitalPoints(request, config)
        val stepper = RungeStepper(sourcePoints.toMutableList(), EarthType.PZ90d02)
        val mode = request.mode.toCalculationMode()
        val calculator = AngularMotionCalculatorFactory.create(mode, stepper, EarthType.PZ90d02, config)
        val surveyId = request.toSurveyId()
        val result = try {
            calculator.calculate(surveyId)
        } catch (error: RuntimeException) {
            throw CustomValidationException(error.message ?: "Ошибка расчета ПУУД")
        }
        val contourWkt = SurveyContourCalculator(EarthType.PZ90d02).calculate(result, 0.0, AngularMotionConfig().focus * atan(request.captureAngleDeg * PI / 180),
            AngularMotionConfig().focus)

        return AngularMotionCalculationResultDTO(
            mode = result.mode,
            satelliteId = request.satelliteId,
            approximateTime = request.approximateTime,
            startTime = toDateTime(result.startTime),
            contourWkt = contourWkt,
            points = result.points.map { point ->
                AngularMotionPointDTO(
                    time = toDateTime(point.t),
                    revolution = point.orbitalPoint.vit.toLong(),
                    tangDeg = point.orientation.tang.toDegrees(),
                    krenDeg = point.orientation.kren.toDegrees(),
                    riskDeg = point.orientation.risk.toDegrees(),
                    groundLatitudeDeg = point.groundPoint?.lat?.toDegrees(),
                    groundLongitudeDeg = point.groundPoint?.long?.toDegrees(),
                    omegaX = point.omega.x,
                    omegaY = point.omega.y,
                    omegaZ = point.omega.z,
                    wdX = point.wd.x,
                    wdY = point.wd.y,
                    wdZ = point.wd.z,
                    sdi = point.sdi,
                    sdiLeft = point.sdiLeft,
                    sdiRight = point.sdiRight,
                    sdiSpread = point.sdiSpread,
                )
            },
        )
    }

    private fun validate(request: AngularMotionCalculationRequestDTO) {
        if (request.durationSec <= 0.0) {
            throw CustomValidationException("durationSec must be positive")
        }
        if (request.captureAngleDeg <= 0.0 || !request.captureAngleDeg.isFinite()) {
            throw CustomValidationException("captureAngleDeg must be positive")
        }
        if (request.latitudeDeg !in -90.0..90.0) {
            throw CustomValidationException("latitudeDeg must be in [-90; 90]")
        }
        if (request.longitudeDeg !in -180.0..180.0) {
            throw CustomValidationException("longitudeDeg must be in [-180; 180]")
        }
        val sdi = request.sdi
        if ((request.mode == AngularMotionModeDTO.AZIMUTH || request.mode == AngularMotionModeDTO.SMOOTH_SDI) &&
            (sdi == null || abs(sdi) < 1.0e-4)
        ) {
            throw CustomValidationException("Для азимутального режима и Smooth SDI требуется ненулевая СДИ")
        }
    }

    private fun validateTargetVisibility(request: AngularMotionCalculationRequestDTO) {
        val windowStart = request.approximateTime.minusMinutes(TARGET_VISIBILITY_WINDOW_MINUTES)
        val windowStop = request.approximateTime.plusMinutes(TARGET_VISIBILITY_WINDOW_MINUTES)
        val visiblePoints = satelliteService.mplPoint(
            ObjViewRequestDTO(
                satellites = listOf(request.satelliteId),
                timeStart = windowStart,
                timeStop = windowStop,
                obj = ObjDTO(
                    id = ANGULAR_MOTION_TARGET_ID,
                    position = PositionDTO(
                        lat = request.latitudeDeg,
                        long = request.longitudeDeg,
                        height = request.heightM,
                    ),
                ),
            ),
        ).collectList().block().orEmpty()

        if (visiblePoints.isEmpty()) {
            throw CustomValidationException(
                "Заданная точка не видна КА ${request.satelliteId} в окрестности времени " +
                    "${request.approximateTime}: ${windowStart} — ${windowStop}. " +
                    "Выберите другое время, точку или спутник.",
            )
        }
    }

    private fun loadOrbitalPoints(
        request: AngularMotionCalculationRequestDTO,
        config: AngularMotionConfig,
    ): List<OrbitalPoint> {
        val reserveBeforeSec = 240L
        val reserveAfterSec = max(240L, request.durationSec.roundToLong() + 240L)
        val stepMs = ((config.stepPuud * 1000.0).roundToLong()).coerceAtLeast(1L)
        val response = satelliteService.exactTime(
            request.satelliteId,
            ExactTimePositionRequestDTO(
                time = request.approximateTime.minusSeconds(reserveBeforeSec),
                timeStop = request.approximateTime.plusSeconds(reserveAfterSec),
                stepMs = stepMs,
            ),
        ).collectList().block().orEmpty()

        if (response.isEmpty()) {
            throw CustomValidationException(
                "Баллистика не вернула точки орбиты для КА ${request.satelliteId} на интервале расчета",
            )
        }
        return response.map { it.toOrbitalPoint() }
    }

    private fun AngularMotionCalculationRequestDTO.toSurveyId(): SurveyId = SurveyId(
        oep = listOf(true, false, false, false),
        t = fromDateTime(approximateTime),
        b = latitudeDeg.toRadians(),
        l = longitudeDeg.toRadians(),
        h = heightM,
        duration = durationSec,
        sdi = sdi?.let { listOf(it) } ?: emptyList(),
        azimuth = azimuthDeg.toRadians(),
        uprAngle = leadAngleDeg.toRadians(),
        pointInCenter = pointInCenter,
    )

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

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

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

    private fun AngularMotionModeDTO.toCalculationMode(): AngularMotionMode = AngularMotionMode.valueOf(name)

    private companion object {
        const val MAP_INTERVAL_MINUTES = 5L
        const val TARGET_VISIBILITY_WINDOW_MINUTES = 15L
        const val ANGULAR_MOTION_TARGET_ID = "angular-motion-target"
    }
}
