package space.nstart.pcp.pcp_request_service.service

import ballistics.Ballistics
import ballistics.orbitalPoints.timeStepper.RungeStepper
import ballistics.types.BallisticsError
import ballistics.types.EarthType
import ballistics.types.FleghtLineSector
import ballistics.types.InitialConditions
import ballistics.types.KeplerParams
import ballistics.types.ModDVType
import ballistics.types.OPKatObj
import ballistics.types.OrbitalPoint
import ballistics.types.PPI
import ballistics.types.PointViewParams
import ballistics.types.TLE
import ballistics.types.VisibilityParametersZRV
import ballistics.types.ZRV
import ballistics.utils.astro.AstronomerJ2000
import ballistics.utils.fromDateTime
import ballistics.utils.math.Vector3D
import ballistics.utils.toDateTime
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.nstart.dep265.tletools.zeptomoby.core.Globals.sqr
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value
import org.springframework.kafka.annotation.KafkaListener
import org.springframework.stereotype.Service
import reactor.core.publisher.Flux
import space.nstart.pcp.pcp_request_service.configuration.CustomErrorException
import space.nstart.pcp.pcp_request_service.configuration.CustomValidationException
import space.nstart.pcp.pcp_request_service.entity.AscNodeEntity
import space.nstart.pcp.pcp_request_service.entity.EarthCellViewsEntity
import space.nstart.pcp.pcp_request_service.entity.EarthCoverageEntity
import space.nstart.pcp.pcp_request_service.entity.FlightLineEntity
import space.nstart.pcp.pcp_request_service.entity.PDCMEntity
import space.nstart.pcp.pcp_request_service.model.SquareViewParamsModel
import space.nstart.pcp.pcp_request_service.repository.AscNodeRepository
import space.nstart.pcp.pcp_request_service.repository.EarthCellViewRepository
import space.nstart.pcp.pcp_request_service.repository.EarthCoverageRepository
import space.nstart.pcp.pcp_request_service.repository.FlightLineRepository
import space.nstart.pcp.pcp_request_service.repository.PDCMRepository
import space.nstart.pcp.pcp_request_service.utils.ContourClipService
import space.nstart.pcp.pcp_request_service.utils.UtcDateTimes
import space.nstart.pcp.pcp_types_lib.dto.ballistics.AscNodeDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.ExactTimePositionRequestDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.InitialConditionsDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.MovementModel
import space.nstart.pcp.pcp_types_lib.dto.ballistics.ObjDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.PointViewParamDTO
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.ballistics.ObjViewRequestDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RadioVisibilityAreaDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteICDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SquareViewParamDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SquareViewParamsWithPointsDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteOrbitAvailabilityDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TLEDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TargetPositionDTO
import tools.jackson.databind.ObjectMapper
import java.time.Clock
import java.time.LocalDateTime
import kotlin.collections.forEach
import kotlin.math.PI
import kotlin.math.abs
import kotlin.math.asin
import kotlin.math.atan
import kotlin.math.atan2
import kotlin.math.log
import kotlin.math.sin
import kotlin.math.sqrt
import kotlin.math.truncate


@Service
class SatelliteService {


    @Value("\${settings.calculation-interval:7}")
    val intervalForward : Int = 7

    @Autowired
    private lateinit var om: ObjectMapper

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

    @Autowired
    private lateinit var pdcmRepository: PDCMRepository

    @Autowired
    private lateinit var satelliteInitialConditionService: SatelliteInitialConditionService

    @Autowired
    private lateinit var ascNodeRepository: AscNodeRepository
    @Autowired
    private lateinit var flightLineRepository: FlightLineRepository
    @Autowired
    private lateinit var earthCoverageRepository: EarthCoverageRepository
    @Autowired
    private lateinit var earthCellViewRepository: EarthCellViewRepository

    @Autowired
    private lateinit var stationCatalogClient: StationCatalogClient

    @Autowired
    private lateinit var clock: Clock

    val astro = AstronomerJ2000(EarthType.PZ90d02)

    @KafkaListener(id = "updatedIcFilter", topics = ["\${spring.kafka.template.default-topic}"], groupId = "\${spring.kafka.consumer.group-id}", filter = "updatedIcFilter")
    fun listener2(data: String?) {

        val tleMessage: TLEDTO =
            try {
                val rootNode = om.readTree(data)
                val payloadNode = rootNode["data"] ?: throw IllegalArgumentException("В сообщении отсутствует поле data")
                om.treeToValue(payloadNode, TLEDTO::class.java)
            }
            catch (e: Exception) {
                logger.error("Ошибка парсинга сообщения от каталога: ${e.message}")
                return
            }

        logger.info("Полученно сообщение об изменении TLE [{}]", tleMessage)

        CoroutineScope(Dispatchers.Default).launch {
            resieveTLE(tleMessage)
        }

        logger.info("Выход из KafkaListener")
    }

    suspend fun resieveTLE(tle: TLEDTO) {
        val bal = Ballistics()
        val params = bal.getTLEParams(TLE(tle.first, tle.second), tle.header ?: "")

        logger.info("Начало расчета баллистики по TLE noradId = ${params.noradId}")

        bal.rollMax = (45.0 * PI / 180.0)
        bal.rollMin = (20.0 *  PI / 180.0)

        var br = bal.calculateOrbPoints(
            TLE(tle1 = tle.first, tle.second),
            86400.0 * intervalForward
        )
        if (br != BallisticsError.OK) {
            logger.warn("Ошибка при расчете ПДЦМ для noradId=${params.noradId}. Код ошибки: $br")
            return
        }

        pdcmRepository.deleteBySatId(params.noradId)
        pdcmRepository.saveAll(bal.points.map { p -> PDCMEntity(p, params.noradId) })
        logger.info("Точки орбиты КА ${params.noradId} сохранены в БД")

        ascNodeRepository.deleteBySatId(params.noradId)
        ascNodeRepository.saveAll(bal.revolutions.map { AscNodeEntity(it, params.noradId) })
        logger.info("ВУЗы орбиты КА ${params.noradId} сохранены в БД")

        val tInt = bal.points.first().t
        br = bal.calculateFlightLine(tInt, tInt + 86400 * intervalForward)
        if (br != BallisticsError.OK) {
            logger.warn("Ошибка при расчете ТП и ПО для ka=${params.noradId}. Код ошибки: $br")
            return
        }
        flightLineRepository.deleteBySatId(params.noradId)
        flightLineRepository.saveAll(bal.flightLine.map { FlightLineEntity(it, params.noradId) })

        logger.info("Параметры трассы полеты и полосы обзора  КА ${params.noradId} сохранены в БД")


        earthCoverageRepository.deleteBySatId(params.noradId)
        earthCoverageRepository.saveAll(bal.coverings.flatMap { (k,v) -> mergeSectors(v).map { EarthCoverageEntity(it, k , params.noradId) } })
        logger.info("Параметры покрытия земной поверхности  КА ${params.noradId} сохранены в БД")

        prepareEarthCovering(bal, params.noradId)

        bal.clear()
        logger.info("Расчет для КА ${params.noradId} завершился успешно")
    }


    private fun InitialConditionsDTO.toIC() = InitialConditions(
        OrbitalPoint(
            t = fromDateTime(orbPoint.time),
            vit = orbPoint.revolution.toInt(),
            r = Vector3D(orbPoint.x,orbPoint.y,orbPoint.z),
            v = Vector3D(orbPoint.vx,orbPoint.vy,orbPoint.vz)
        ),
        f81 = f81,
        sBall = sBall
    )

    suspend fun resieveRV(rv: SatelliteICDTO) {
        val savedInitialCondition = satelliteInitialConditionService.saveReceived(rv)
        val bal = Ballistics()
        bal.modDVType = rv.movementModel.toModDVType()

        logger.info("Начало расчета баллистики по RV id = ${rv.satelliteId}, model = ${rv.movementModel}")

        bal.rollMax = (45.0 * PI / 180.0)
        bal.rollMin = (20.0 *  PI / 180.0)

        var br = bal.calculateOrbPoints(
            rv.ic.toIC(),
            fromDateTime(rv.ic.orbPoint.time),
            fromDateTime(rv.ic.orbPoint.time)+ 86400 * intervalForward

        )
        if (br != BallisticsError.OK) {
            logger.warn("Ошибка при расчете ПДЦМ для noradId=${rv.satelliteId}. Код ошибки: $br")
            return
        }

        pdcmRepository.deleteBySatId(rv.satelliteId)
        pdcmRepository.saveAll(bal.points.map { p -> PDCMEntity(p, rv.satelliteId) })
        logger.info("Точки орбиты КА ${rv.satelliteId} сохранены в БД")

        ascNodeRepository.deleteBySatId(rv.satelliteId)
        ascNodeRepository.saveAll(bal.revolutions.map { AscNodeEntity(it, rv.satelliteId) })
        logger.info("ВУЗы орбиты КА ${rv.satelliteId} сохранены в БД")

        val tInt = bal.points.first().t
        br = bal.calculateFlightLine(tInt, tInt + 86400 * intervalForward)
        if (br != BallisticsError.OK) {
            logger.warn("Ошибка при расчете ТП и ПО для ka=${rv.satelliteId}. Код ошибки: $br")
            return
        }
        flightLineRepository.deleteBySatId(rv.satelliteId)
        flightLineRepository.saveAll(bal.flightLine.map { FlightLineEntity(it, rv.satelliteId) })

        logger.info("Параметры трассы полеты и полосы обзора  КА ${rv.satelliteId} сохранены в БД")


        earthCoverageRepository.deleteBySatId(rv.satelliteId)
        earthCoverageRepository.saveAll(bal.coverings.flatMap { (k,v) -> mergeSectors(v).map { EarthCoverageEntity(it, k , rv.satelliteId) } })
        logger.info("Параметры покрытия земной поверхности  КА ${rv.satelliteId} сохранены в БД")

        prepareEarthCovering(bal, rv.satelliteId)

        satelliteInitialConditionService.markLastCalculated(savedInitialCondition)

        bal.clear()
        logger.info("Расчет для КА ${rv.satelliteId} завершился успешно")
    }

    fun currentInitialConditions(satelliteId: Long): SatelliteICDTO? =
        satelliteInitialConditionService.current(satelliteId)

    private fun MovementModel.toModDVType(): ModDVType =
        when (this) {
            MovementModel.FOTO -> ModDVType.FOTO
            MovementModel.KONDOR -> ModDVType.KONDOR
            MovementModel.METEORM1 -> ModDVType.METEORM1
            MovementModel.METEORM2 -> ModDVType.METEORM2
            MovementModel.BARS -> ModDVType.BARS
            MovementModel.KONDOR_PROGNOZ -> ModDVType.KONDOR_PROGNOZ
        }


    fun mergeSectors(sectors: List<FleghtLineSector>): List<FleghtLineSector> {
        if (sectors.isEmpty()) return emptyList()

        val sorted = sectors.sortedBy { it.tStart }
        val result = mutableListOf<FleghtLineSector>()
        var current = sorted[0]

        for (i in 1 until sorted.size) {
            val next = sorted[i]
            if (abs(next.tStart - current.tStop) < 2) {
                // Объединяем: расширяем tStop текущего сектора
                current = FleghtLineSector(current.tStart, maxOf(current.tStop, next.tStop))
            } else {
                result.add(current)
                current = next
            }
        }
        result.add(current)

        return result
    }


    fun clear(id : Long): SatelliteBallisticsDeletionSummary {
        val pdcmDeleted = pdcmRepository.deleteBySatId(id)
        val ascNodesDeleted = ascNodeRepository.deleteBySatId(id)
        val flightLinesDeleted = flightLineRepository.deleteBySatId(id)
        val earthCoverageDeleted = earthCoverageRepository.deleteBySatId(id)
        val earthCellViewsDeleted = earthCellViewRepository.deleteBySatId(id)
        val summary = SatelliteBallisticsDeletionSummary(
            satelliteId = id,
            pdcmDeleted = pdcmDeleted,
            ascNodesDeleted = ascNodesDeleted,
            flightLinesDeleted = flightLinesDeleted,
            earthCoverageDeleted = earthCoverageDeleted,
            earthCellViewsDeleted = earthCellViewsDeleted
        )
        logger.info(
            "Deleted ballistics data for satellite: satelliteId={}, pdcm={}, ascNodes={}, flightLines={}, earthCoverage={}, earthCellViews={}",
            summary.satelliteId,
            summary.pdcmDeleted,
            summary.ascNodesDeleted,
            summary.flightLinesDeleted,
            summary.earthCoverageDeleted,
            summary.earthCellViewsDeleted
        )
        return summary
    }

    fun exactTime(satId : Long, request : ExactTimePositionRequestDTO) = Flux.create {
        sink ->
        logger.info("Запрос расчета выхода на заданное время для КА $satId ")
        val points = pdcmRepository.findBySatelliteIdAndTimeBetween(
            satId,
            request.time.minusSeconds(100),
            request.timeStop?.plusSeconds(100) ?: request.time.plusSeconds(100)
        )
        if (points.isEmpty())
            throw CustomErrorException("Нет точек орбиты для КА $satId на интервале ${request.time} - ${request.timeStop ?: request.time}")
        val stepper = RungeStepper(points.map { p -> p.toOrbitalPoint() }.toMutableList(), EarthType.PZ90d02)
        var t = request.time
        val tk = request.timeStop ?: t.plusNanos(5)
        var step = request.stepMs ?: 125
        if (step < 1)
            step = 125
        while (t <= tk) {

            val op = stepper.calculate(fromDateTime(t))
            if (op == null) {
                logger.error("Ошибка расчета выхода на заданное время $t")
                throw CustomValidationException("Внутренняя ошибка расчета выхода на заданное время")
            }

            sink.next( OrbPointDTO(
                toDateTime(op.t),
                op.vit.toLong(),
                op.v.x,
                op.v.y,
                op.v.z,
                op.r.x,
                op.r.y,
                op.r.z)
            )
            t = t.plusNanos(step * 1000000)
        }
        if (request.timeStop != null && t.minusNanos(step * 1000000) < tk) {
            val op = stepper.calculate(fromDateTime(tk))
            if (op == null) {
                logger.error("Ошибка расчета выхода на заданное время $t")
                throw CustomValidationException("Внутренняя ошибка расчета выхода на заданное время")
            }

            sink.next(OrbPointDTO(
                toDateTime(op.t),
                op.vit.toLong(),
                op.v.x,
                op.v.y,
                op.v.z,
                op.r.x,
                op.r.y,
                op.r.z)
            )
        }

        sink.complete()
    }



    fun getCellCovering(num : Long) : List<SquareViewParamsWithPointsDTO>{
        return earthCellViewRepository.findByCellNum(num).map { view ->
            val points = pdcmRepository.findBySatelliteIdAndTimeBetween(
            view.satelliteId,
            view.timeBegin.minusSeconds(100),
                view.timeEnd.plusSeconds(100)
            )
            SquareViewParamsWithPointsDTO(
                view.toDTO(),
                points.map { it.toDTO() }
            )
        }
    }


    fun mplPoint(req : ObjViewRequestDTO) : Flux<PointViewParamDTO>{

        return Flux.fromIterable(
            req.satellites
                .flatMap { sat ->
                    val bal = Ballistics()
                    bal.setOrbitalPoints(
                        loadOrbitalPoints(sat, req.timeStart, req.timeStop),
                        EarthType.PZ90d02
                    )

                    bal.rollMin = 0.0
                    bal.rollMax = 45.0 / 180 * PI

                    val indexes = mkCovIndexes(listOf(req.obj.position?: PositionDTO()))
                    val sectors = loadEarthCoverageSectors(sat, req.timeStart, req.timeStop, indexes)

                    bal.setEarthCoverage(
                        sectors
                    )

                    val t1 = req.timeStart
                    val t2 = req.timeStop
                    bal.sunAngleMin = req.sunAngleMin?.let { it / 180 * PI } ?: (-PI / 2.0)

                    val r = bal.calculateMPL(
                        fromDateTime(t1),
                        fromDateTime(t2),
                        listOf(OPKatObj(0,0,"",0,
                            (req.obj.position?.lat?:0.0) * PI / 180,
                            (req.obj.position?.long?:0.0) * PI / 180,
                            req.obj.position?.height?:0.0,
                            0.0, 0.0, 0.0
                            )
                        )
                    )
                    if (r != BallisticsError.OK) {
                        logger.error("Ошибка при расчете параметров наблюдения объекта (${req.obj}) для КА $sat. Код ошибки : $r")
                        listOf()
                    } else
                        bal.mpl.map { PointViewParamDTO(
                            noradId = sat,
                            objectId = req.obj.id?:"",
                            time = toDateTime(it.traverz),
                            revolution = it.vit.toLong(),
                            gamma = it.orientation.kren * 180 / PI,
                            sunAngle = it.sunAngle * 180 / PI,
                            range = it.range,
                            revSign = if (it.pv ==0 ) RevolutionSign.ASC else RevolutionSign.DESC
                        ) }

                }
        )
    }


    fun mplSquare(req : ObjViewRequestDTO) : Flux<SquareViewParamDTO>{

        logger.info("Запрос МПЛ по площадному объекту")
       return Flux.fromIterable(
            req.satellites
                .flatMap { sat ->
                    val bal = Ballistics()

                    bal.setOrbitalPoints(
                        loadOrbitalPoints(sat, req.timeStart, req.timeStop),
                        EarthType.PZ90d02
                    )

                    bal.rollMin = 0.0
                    bal.rollMax = 45.0 / 180 * PI

                    val contourService = ContourClipService()
                    val contour = contourService.getObjContour(req.obj)
                    if (contour == null || contour.count() < 3) {
                        logger.error("Контур площадного объекта пуст или содержит меньше 3-х точек")
                        listOf()
                    }
                    else {
                        val indexes = mkCovIndexes(contour)
                        val sectors = loadEarthCoverageSectors(sat, req.timeStart, req.timeStop, indexes)

                        bal.setEarthCoverage(
                            sectors
                        )
                        val t1 = req.timeStart
                        val t2 = req.timeStop
                        bal.sunAngleMin = req.sunAngleMin?.let { it / 180 * PI } ?: (-PI / 2.0)


                        val r = bal.calculateMPL(
                            fromDateTime(t1),
                            fromDateTime(t2),
                            contourService.splitPolySides(contour.map { p ->
                                PositionDTO(
                                    p.lat / 180 * PI,
                                    p.long / 180 * PI,
                                    p.height
                                )
                            })
                        )
                        if (r != BallisticsError.OK) {
                            logger.error("Ошибка при расчете параметров наблюдения площадного объекта (${req.obj}) для КА $sat. Код ошибки : $r")
                            listOf()
                        } else
                            squareViewFromMPL(sat, bal.mpl, req.obj.id ?: "")
                    }
                }
        ).map { square -> square.toDTO() }
    }


    private fun squareViewFromMPL(id : Long, mpl: Iterable<PointViewParams>, n: String): Iterable<SquareViewParamsModel>{

        val svp = mutableListOf<SquareViewParamsModel>()
        val vp = SquareViewParamsModel(
            tN = -1.0,
            tK = 0.0,
            objID = n,
            id = id
        )
        sortPointViews(mpl).forEach { m ->
            if (m.traverz - vp.tK > 60.0*3) {
                // начало нового интервала наблюдения! Надо сохранить старый, если такой есть.
                if (vp.tN > 0)
                    svp.add(vp.clone())

                vp.tN = m.traverz
                vp.vitN = m.vit
                vp.tK = m.traverz
                vp.vitK = m.vit
                vp.pvN = if (m.pv == 0) RevolutionSign.ASC else RevolutionSign.DESC
                vp.pvK = if (m.pv == 0) RevolutionSign.ASC else RevolutionSign.DESC
                vp.krenMin = m.orientation.kren
                vp.krenMax = m.orientation.kren
            } else {
                vp.tK = m.traverz
                vp.vitK = m.vit

                vp.pvK = if (m.pv == 0) RevolutionSign.ASC else RevolutionSign.DESC
                if (vp.krenMax < m.orientation.kren)
                    vp.krenMax = m.orientation.kren

                if (vp.krenMin > m.orientation.kren)
                    vp.krenMin = m.orientation.kren
            }
        }
        if (vp.tN > 0)
            svp.add(vp.clone())
        return svp
    }

    internal fun sortPointViews(mpl: Iterable<PointViewParams>): List<PointViewParams> =
        mpl.sortedWith(
            compareBy<PointViewParams>(
                { it.traverz },
                { it.vit },
                { it.pointNumb },
                { it.pv },
                { it.orientation.kren }
            )
        )

    private fun loadOrbitalPoints(satelliteId: Long, timeStart: LocalDateTime, timeStop: LocalDateTime) =
        pdcmRepository.findBySatelliteIdAndTimeBetween(satelliteId, timeStart, timeStop)
            .sortedWith(
                compareBy<PDCMEntity>(
                    { it.time },
                    { it.revolution },
                    { it.id ?: Long.MAX_VALUE }
                )
            )
            .map { it.toOrbitalPoint() }

    private fun loadEarthCoverageSectors(
        satelliteId: Long,
        timeStart: LocalDateTime,
        timeStop: LocalDateTime,
        cellIndexes: List<Long>
    ): Map<Int, MutableList<FleghtLineSector>> =
        earthCoverageRepository.findBySatelliteIdAndTStartGreaterThanAndTStopLessThanAndCellNumberIn(
            satelliteId,
            timeStart,
            timeStop,
            cellIndexes
        )
            .sortedWith(
                compareBy<EarthCoverageEntity>(
                    { it.cellNumber },
                    { it.tStart },
                    { it.tStop },
                    { it.id ?: Long.MAX_VALUE }
                )
            )
            .groupBy { it.cellNumber.toInt() }
            .entries
            .sortedBy { it.key }
            .associateTo(linkedMapOf()) { (cellNumber, views) ->
                cellNumber to views.map { view ->
                    FleghtLineSector(
                        fromDateTime(view.tStart),
                        fromDateTime(view.tStop)
                    )
                }.toMutableList()
            }




    fun getBInd(b: Double): Long {
        var b2: Double = b * 180.0 / PI
        b2 = 90.0 - b2
        var k: Long = truncate(b2 / 2.0).toLong()
        if (k > 89) k = 89
        return k
    }

    fun getLInd(l2: Double): Long {
        var l: Double = l2 * 180.0 / PI
        if (l < 0) l += 360.0
        var k: Long = truncate(l / 2.0).toLong()
        if (k > 179) k = 179
        return k
    }

    fun getInd(
        b: Double,
        l: Double,
    ): Long {
        return getBInd(b) * 180 + getLInd(l)
    }


    fun mkCovIndexes(contour : Iterable<PositionDTO>) : List<Long>{
        val indexes = mutableListOf<Long>()
        var bMin = 100.0
        var bMax = -100.0
        var lMin = 370.0
        var lMax = -370.0

        contour.forEach { c ->
            val b = c.lat
            val l = c.long

            if (b < bMin) bMin = b
            if (b > bMax) bMax = b
            if (l < lMin) lMin = l
            if (l > lMax) lMax = l
        }

        while (bMin < bMax + 0.6){

            var l = lMin
            while (l < lMax + 0.6){
                val ind  = getInd(bMin * PI / 180, l * PI / 180)
                if (!indexes.contains(ind))
                    indexes.add(ind)
                l += 0.5
            }

            bMin += 0.5
        }
        return indexes
    }






    fun getBInd2(b: Double): Long {
        var b2: Double = b * 180.0 / PI
        b2 = 90.0 - b2
        var k: Long = truncate(b2).toLong()
        if (k > 180) k = 180
        return k
    }

    fun getLInd2(l2: Double): Long {
        var l: Double = l2 * 180.0 / PI
        if (l < 0) l += 360.0
        var k: Long = truncate(l).toLong()
        if (k > 359) k = 359
        return k
    }

    fun getInd2(
        b: Double,
        l: Double,
    ): Long {
        return getBInd2(b) * 360 + getLInd2(l)
    }




    fun prepareEarthCovering(bal : Ballistics, satId : Long){

        logger.info("Начало расчета покрытия ячеек земной поверхности для КА $satId")

        earthCellViewRepository.deleteBySatId(satId)
        logger.info("Функционал не используется. Старые результаты удалены.")
        return

//        val contourService = ContourClipService()
//        val step = 1.0
//
//        bal.sunAngleMin = -PI/2
//
//        val num  : Long = 1
//        var b = -90 + step
//        while (b <= 90){
//            var l =  0.0
//            val line = mutableListOf<EarthCellViewsEntity>()
//            while (l <= 360){
//                val cellInd = getInd2(b * PI / 180.0,l* PI / 180.0)
//                val t1 = bal.points.first().t
//                val t2 = bal.points.last().t
//                val cont : String = "POLYGON (($l $b, ${l-1} ${b}, ${l-1} ${b-1}, $l ${b-1}, $l $b))"
//                val contour = contourService.getObjContour(ObjDTO(contourWKT = cont))
//                if (contour == null || contour.count() < 3) {
//                    logger.error("Контур площадного объекта пуст или содержит меньше 3-х точек")
//                    continue
//                }
//
//                val objs = contourService.splitPolySides(contour.map { p ->
//                    PositionDTO(
//                        p.lat / 180 * PI,
//                        p.long / 180 * PI,
//                        p.height
//                    )
//                }
//                )
//                val r = bal.calculateMPL(
//                    t1,
//                    t2,
//                    objs
//
//                )
//                val views =
//                if (r != BallisticsError.OK) {
//                    logger.error("Ошибка при расчете параметров наблюдения площадного объекта (${cellInd}) для КА $satId. Код ошибки : $r")
//                    listOf()
//                } else
//                    squareViewFromMPL(satId, bal.mpl, "")
//
//                for (view in views) {
//                    line.add(
//                        EarthCellViewsEntity(
//                            null,
//                            satId,
//                            cellInd,
//                            view.vitN.toLong(),
//                            toDateTime(view.tN),
//                            if(view.pvN == RevolutionSign.ASC) 0 else 1,
//                            view.vitK.toLong(),
//                            toDateTime(view.tK),
//                            if(view.pvK == RevolutionSign.ASC) 1 else 0,
//                            view.krenMin * 180 / PI,
//                            view.krenMax * 180 / PI,
//                            cont
//                        )
//                    )
//                }
//                l += step
//                if (line.isEmpty()) {
//                    logger.info("нет наблюдений ячейки $cont")
//                }
//                earthCellViewRepository.saveAll(line)
//                line.clear()
//            }
//            b += step
//        }
//        logger.info("Конец расчета покрытия ячеек земной поверхности для КА $satId")
    }

    fun getOrbPoints(id : Long, tn : LocalDateTime?, tk : LocalDateTime?) =
        pdcmRepository.findBySatelliteIdAndTimeBetween(
            id,
            tn?: UtcDateTimes.now(clock),
            tk?: UtcDateTimes.now(clock).plusDays(1))
            .map { it.toDTO()
            }

    fun getOrbitAvailability(time: LocalDateTime?): List<SatelliteOrbitAvailabilityDTO> =
        if (time == null) {
            pdcmRepository.findSatelliteOrbitAvailability()
        } else {
            pdcmRepository.findSatelliteOrbitAvailabilityAtTime(time)
        }

    fun getOrbitAvailabilityForDate(time: LocalDateTime): List<SatelliteOrbitAvailabilityDTO> {
        val dayStart = time.toLocalDate().atStartOfDay()
        return pdcmRepository.findSatelliteOrbitAvailabilityForDate(dayStart, dayStart.plusDays(1))
    }

    fun getFL(id : Long, tn : LocalDateTime?, tk : LocalDateTime?, step : Double?) =
        flightLineRepository.findBySatelliteIdAndTimeBetween(id,
            tn?: UtcDateTimes.now(clock),
            tk?: UtcDateTimes.now(clock).plusDays(1)
        )
            .map { it.toDTO() }



    fun getRVA(id : Long, tn : LocalDateTime, tk : LocalDateTime) : List<RadioVisibilityAreaDTO> {

        val bal = Ballistics()

        val points = pdcmRepository.findBySatelliteIdAndTimeBetween(id,
            tn,
            tk).map { it.toOrbitalPoint() }

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

        bal.setOrbitalPoints(points,
            EarthType.PZ90d02)

        val tstart = points.first().t
        val tstop = points.last().t

        bal.rollMin = 0.0
        bal.rollMax = 45.0 / 180 * PI

        // ЗРВ считаем по реальным станциям приёма — каталог берём у pcp-stations-service.
        // Идентификатор PPI = station.number, поэтому RadioVisibilityAreaDTO.stationId
        // совпадает с номером станции (клиент сопоставляет название по своему списку станций).
        val stations = stationCatalogClient.allStations()
        if (stations.isEmpty()) {
            logger.warn("Нет зарегистрированных станций — ЗРВ для КА {} не рассчитываются", id)
            return emptyList()
        }

        val ppis = stations.map { station ->
            PPI(
                station.number, 1,
                station.position.lat * PI / 180,
                station.position.long * PI / 180,
                station.position.height,
                station.elevationMin * PI / 180,
                station.elevationMax * PI / 180
            )
        }

        val r = bal.calculateZRV(ppis, tstart, tstop)
        if (r != BallisticsError.OK) {
            logger.error("Ошибка при расчете параметров ЗРВ : $r")
            return listOf()
        } else
            return bal.zrv.map { mkRVA(it, id) }.sortedBy { it.onStart.time }

    }

    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),
        duration = (p.zoneOut.t - p.zoneIn.t)
    )
    fun mkTP(p : VisibilityParametersZRV) = TargetPositionDTO(
        toDateTime(p.t),
        p.elevation * 180 / PI,
        p.azimuth * 180 / PI,
        p.range
    )



    fun calculateKeplerParams(point: OrbitalPoint): KeplerParams {
        val res = KeplerParams()

        val aL00 = 6.25648106E+7
        val ask = astro.grinvToASK(point)

        val r = sqrt(sqr(ask.r.x) + sqr(ask.r.y) + sqr(ask.r.z))
        val v = sqrt(sqr(ask.v.x) + sqr(ask.v.y) + sqr(ask.v.z))

        val mu = astro.earth.fM
        val k = (r * sqr(v)) / mu

        val sinO = (ask.r.x * ask.v.x + ask.r.y * ask.v.y + ask.r.z * ask.v.z) / (r * v)
        val cosO = sqrt(1 - sqr(sinO))

        val c1 = ask.r.y * ask.v.z - ask.r.z * ask.v.y
        val c2 = ask.r.z * ask.v.x - ask.r.x * ask.v.z
        val c3 = ask.r.x * ask.v.y - ask.r.y * ask.v.x
        val c = sqrt(sqr(c1) + sqr(c2) + sqr(c3))

        res.ael = r / (2 - k)
        val lambda = (1 / res.ael) * sqrt(mu / res.ael)

        res.t = (2 * PI * res.ael * sqrt(res.ael)) / sqrt(astro.earth.middleRadius * aL00)
        res.e = sqrt(1 - k * (2 - k) * (1 - sinO * sinO))
        res.v = atan((k * sinO * cosO) / (k * sqr(cosO) - 1))
        res.u = atan2((ask.r.z * c), (ask.r.y * c1 - ask.r.x * c2))
        res.eA = atan((sqrt(1 - sqr(res.e)) * k * sinO * cosO) / (sqr(res.e) + (k * sqr(cosO) - 1)))
        res.tau = ask.t + ((res.e * sin(res.eA) - res.eA) / lambda)
        res.nakl = 0.5 * PI - asin(c3 / c)
        res.omegab = atan2(c1, -c2)
        res.omegam = res.u - res.v
        res.o = asin(sinO)
        res.rA = res.ael * (1 + res.e)
        res.rP = res.ael * (1 - res.e)
        res.dmv = point.t

        if (res.omegam < 0) res.omegam += 2 * PI
        if (res.tau < 0) res.tau += 1

        return res
    }

    fun getAscNodes(id : Long, tn : LocalDateTime?, tk : LocalDateTime?) : List<AscNodeDTO> {

        return  ascNodeRepository.findBySatelliteIdAndTimeBetween(
            id,
            tn ?: UtcDateTimes.now(clock),
            tk ?: UtcDateTimes.now(clock).plusDays(1)
        )
            .map {
                val keps = calculateKeplerParams(
                    OrbitalPoint(
                        fromDateTime(it.time),
                        1,
                        Vector3D(it.x,it.y,it.z),
                        Vector3D(it.vx,it.vy,it.vz)
                ))
                it.toDTO(keps, astro.earth.ekvRadius)
            }
    }
}

data class SatelliteBallisticsDeletionSummary(
    val satelliteId: Long,
    val pdcmDeleted: Int,
    val ascNodesDeleted: Int,
    val flightLinesDeleted: Int,
    val earthCoverageDeleted: Int,
    val earthCellViewsDeleted: Int
)



