package space.nstart.pcp.pcp_request_service.service

import ballistics.Ballistics
import ballistics.orbitalPoints.timeStepper.TLEStepper
import ballistics.types.BallisticsError
import ballistics.types.EarthType
import ballistics.types.KeplerParams
import ballistics.types.PPI
import ballistics.types.RevolutionParameter
import ballistics.types.TLE
import ballistics.types.TLEParams
import ballistics.types.VisibilityParametersZRV
import ballistics.types.ZRV
import ballistics.utils.fromDateTime
import ballistics.utils.toDateTime
import org.springframework.stereotype.Service
import space.nstart.pcp.pcp_request_service.configuration.CustomErrorException
import space.nstart.pcp.pcp_request_service.configuration.CustomValidationException
import space.nstart.pcp.pcp_types_lib.dto.ballistics.AscNodeDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.KeplersDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RadioVisibilityAreaDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TLEDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TlePointRequestDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TleRevolutionsRequestDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TargetPositionDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TleRvaRequestDTO
import java.time.LocalDateTime
import kotlin.math.PI


@Service
class TLEService() {
    private companion object {
        const val SECONDS_IN_DAY = 86_400.0
        const val PZ90_EQUATORIAL_RADIUS = 6_378_136.0
    }

    fun mkTP(p : VisibilityParametersZRV) = TargetPositionDTO(
        toDateTime(p.t),
        p.elevation * 180 / PI,
        p.azimuth * 180 / PI,
        p.range
    )

    fun mkRVA(p : ZRV, id : Long) = RadioVisibilityAreaDTO(
        noradId = id,
        stationId = p.ppi.toLong(),
        revolution = p.vit.toLong(),
        onStart = mkTP(p.zoneIn),
        onMaximum = mkTP(p.zoneMax),
        onStop = mkTP(p.zoneOut)
    )


    fun parseTLE(tle: TLEDTO) : TLEParams{
        val bal = Ballistics()
        return bal.getTLEParams(TLE(tle.first, tle.second), tle.header?:"empty")
    }




    fun point(req: TlePointRequestDTO): OrbPointDTO {
        val bal = Ballistics()
        val tleParams = try {
            bal.getTLEParams(TLE(req.tle.first, req.tle.second), req.tle.header ?: "empty")
        } catch (ex: Exception) {
            throw CustomValidationException("Ошибка формата TLE : ${ex.message}")
        }
        if (req.time < tleParams.time) {
            throw CustomValidationException("Время расчета должно быть не раньше эпохи TLE ${tleParams.time}")
        }

        val point = try {
            val stepper = TLEStepper(req.tle.first, req.tle.second, EarthType.PZ90d02)
            stepper.calculate(fromDateTime(req.time))
        } catch (ex: Exception) {
            throw CustomErrorException("Ошибка расчета положения по TLE : ${ex.message}")
        }

        return OrbPointDTO(
            toDateTime(point.t),
            point.vit.toLong(),
            point.v.x,
            point.v.y,
            point.v.z,
            point.r.x,
            point.r.y,
            point.r.z
        )
    }

    fun rva(req : TleRvaRequestDTO) : Iterable<RadioVisibilityAreaDTO> {

        val bal = Ballistics()
        var tleParams : TLEParams
        try {
            tleParams = bal.getTLEParams(TLE(req.tle.first, req.tle.second))
            if (req.timeStart >= req.timeStop || req.timeStart < tleParams.time)
                throw CustomValidationException("Ошибка задания интервала расчета")
        }catch (ex : Exception){
            throw CustomValidationException("Ошибка формата TLE : ${ex.message}")
        }
        val duration = fromDateTime(req.timeStop) - fromDateTime(req.timeStart)
        val r = bal.calculateOrbPoints(TLE(req.tle.first, req.tle.second), duration)

        if (r != BallisticsError.OK)
            throw CustomErrorException("Ошибка расчета ПДЦМ : $r")

        val rz = bal.calculateZRV(
            listOf(PPI(
                req.station.number.toInt(),
                0,
                req.station.position.lat * PI / 180.0,
                req.station.position.long * PI / 180.0,
                req.station.position.height,
                req.station.elevationMin * PI / 180.0,
                req.station.elevationMax * PI / 180.0)
            ),
            fromDateTime(req.timeStart),
            fromDateTime(req.timeStop)
        )

        if (rz != BallisticsError.OK)
            throw CustomErrorException("Ошибка расчета зон радио-видимости : $rz")

        return bal.zrv.map { mkRVA(it, tleParams.noradId) }
    }

    fun revolutions(req: TleRevolutionsRequestDTO): List<AscNodeDTO> {
        if (!req.days.isFinite() || req.days <= 0.0) {
            throw CustomValidationException("Интервал расчета должен быть больше 0 суток")
        }

        val bal = Ballistics()
        val tle = TLE(req.tle.first, req.tle.second)
        try {
            bal.getTLEParams(tle, req.tle.header ?: "empty")
        } catch (ex: Exception) {
            throw CustomValidationException("Ошибка формата TLE : ${ex.message}")
        }

        val result = bal.calculateOrbPoints(tle, req.days * SECONDS_IN_DAY)
        if (result != BallisticsError.OK) {
            throw CustomErrorException("Ошибка расчета витков по TLE : $result")
        }

        return bal.revolutions.map { mkAscNode(it, bal.calculateKeplerParams(it.vuz)) }
    }

    private fun mkAscNode(revolution: RevolutionParameter, keps: KeplerParams) = AscNodeDTO(
        time = toDateTime(revolution.vuz.t),
        revolution = revolution.vuz.vit,
        long = revolution.lVuz * 180 / PI,
        height = revolution.hVuz,
        x = revolution.vuz.r.x,
        y = revolution.vuz.r.y,
        z = revolution.vuz.r.z,
        vx = revolution.vuz.v.x,
        vy = revolution.vuz.v.y,
        vz = revolution.vuz.v.z,
        keps = KeplersDTO(
            (keps.ael - PZ90_EQUATORIAL_RADIUS) / 1000.0,
            keps.e,
            keps.nakl * 180 / PI,
            keps.omegab * 180 / PI,
            keps.omegam * 180 / PI
        ),
        localMeanTime = tmest(toDateTime(revolution.vuz.t), revolution.lVuz)
    )

    private fun tmest(t: LocalDateTime, longitudeRad: Double): LocalDateTime {
        var longitude = longitudeRad
        if (longitude > PI) {
            longitude -= 2 * PI
        }
        return t.plusSeconds(-60 * 60 * 3)
            .plusNanos((longitude / 2.0 / PI * SECONDS_IN_DAY * 1e9).toLong())
    }
}
