package space.nstart.pcp.slots_service.service

import ballistics.Ballistics
import ballistics.orbitalPoints.timeStepper.TLEStepper
import ballistics.types.EarthType
import ballistics.types.TLE
import ballistics.utils.fromDateTime
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TLEDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TLEExtensionDTO
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteSummaryDTO
import space.nstart.pcp.slots_service.configuration.CustomErrorException
import space.nstart.pcp.slots_service.dto.tleanalysis.RadiusVectorDTO
import space.nstart.pcp.slots_service.dto.tleanalysis.RadiusVectorDiscrepancyDTO
import space.nstart.pcp.slots_service.dto.tleanalysis.TleAnalysisSatelliteDTO
import space.nstart.pcp.slots_service.dto.tleanalysis.TleAnalysisResponseDTO
import space.nstart.pcp.slots_service.dto.tleanalysis.TleAnalysisRowDTO
import java.time.Duration
import java.time.LocalDateTime
import kotlin.math.sqrt
import kotlin.system.measureTimeMillis

@Service
class TleAnalysisService(
    private val tleMonitoringService: TleMonitoringService,
    private val satelliteCatalogService: SatelliteCatalogService
) {
    private val logger = LoggerFactory.getLogger(this::class.java)

    fun satellites(): List<TleAnalysisSatelliteDTO> {
        logger.info("TLE analysis satellites list stage started")
        var catalogSatellites = emptyList<SatelliteSummaryDTO>()
        val catalogLoadMs = measureTimeMillis {
            catalogSatellites = runCatching { satelliteCatalogService.allSatellites() }
                .onFailure { exception ->
                    logger.warn("Не удалось загрузить спутники из satellite-catalog-service для анализа TLE: {}", exception.message)
                }
                .getOrDefault(emptyList())
        }
        var tleRecords = emptyList<TLEExtensionDTO>()
        val tleLoadMs = measureTimeMillis {
            tleRecords = runCatching { tleMonitoringService.allTles() }
                .onFailure { exception ->
                    logger.warn("Не удалось загрузить TLE из tle-monitoring-service для списка анализа TLE: {}", exception.message)
                }
                .getOrDefault(emptyList())
        }
        logger.info(
            "TLE analysis satellites list sources loaded: catalogSatellites={}, tleRecords={}, catalogLoadMs={}, tleLoadMs={}",
            catalogSatellites.size,
            tleRecords.size,
            catalogLoadMs,
            tleLoadMs
        )

        if (tleRecords.isEmpty()) {
            val fallbackSatellites = catalogSatellites.mapNotNull { satellite -> satellite.toAnalysisSatelliteFallback() }
            logger.info(
                "TLE analysis satellites list built from catalog fallback: satellites={}",
                fallbackSatellites.size
            )
            return fallbackSatellites
        }

        val catalogByNoradId = catalogSatellites
            .mapNotNull { satellite -> satellite.noradId?.let { noradId -> noradId to satellite } }
            .toMap()
        val catalogById = catalogSatellites.associateBy { it.id }

        val satellites = tleRecords
            .groupingBy { record -> record.satelliteId }
            .eachCount()
            .entries
            .sortedBy { (satelliteId, _) -> satelliteId }
            .map { (tleSatelliteId, recordsCount) ->
                val catalogSatellite = catalogByNoradId[tleSatelliteId] ?: catalogById[tleSatelliteId]
                catalogSatellite?.toAnalysisSatellite(
                    tleSatelliteId = tleSatelliteId,
                    recordsCount = recordsCount
                ) ?: TleAnalysisSatelliteDTO(
                    id = tleSatelliteId,
                    noradId = tleSatelliteId,
                    name = "КА $tleSatelliteId",
                    tleRecordsCount = recordsCount
                )
            }
        logger.info(
            "TLE analysis satellites list built from TLE records: satellites={}, matchedCatalogSatellites={}, unmatchedTleSatellites={}",
            satellites.size,
            satellites.count { it.catalogId != null },
            satellites.count { it.catalogId == null }
        )
        return satellites
    }

    fun analyzeSatellite(satelliteId: Long, tleCount: Int = DEFAULT_TLE_ANALYSIS_COUNT): TleAnalysisResponseDTO {
        val normalizedTleCount = tleCount.coerceAtLeast(1)
        logger.info(
            "TLE analysis report stage started: satelliteId={}, requestedTleCount={}, normalizedTleCount={}",
            satelliteId,
            tleCount,
            normalizedTleCount
        )

        var sourceRecords = emptyList<TLEExtensionDTO>()
        val sourceLoadMs = measureTimeMillis {
            sourceRecords = tleMonitoringService.satelliteTles(satelliteId)
        }
        logger.info(
            "TLE analysis report source TLE loaded: satelliteId={}, records={}, durationMs={}",
            satelliteId,
            sourceRecords.size,
            sourceLoadMs
        )

        val parsedRecords = sourceRecords.mapNotNull { parseRecord(it) }.sortedBy { it.epoch }
        val skippedRecords = sourceRecords.size - parsedRecords.size
        if (skippedRecords > 0) {
            logger.warn(
                "TLE analysis report skipped invalid TLE records: satelliteId={}, skipped={}, sourceRecords={}",
                satelliteId,
                skippedRecords,
                sourceRecords.size
            )
        }

        val records = parsedRecords.takeLast(normalizedTleCount)
        logger.info(
            "TLE analysis report records selected: satelliteId={}, parsedRecords={}, selectedRecords={}, firstEpoch={}, lastEpoch={}",
            satelliteId,
            parsedRecords.size,
            records.size,
            records.firstOrNull()?.epoch,
            records.lastOrNull()?.epoch
        )

        var rows = emptyList<TleAnalysisRowDTO>()
        val rowsBuildMs = measureTimeMillis {
            rows = records.mapIndexed { index, record ->
                // Сравнение пары TLE выполняется на эпоху более поздней записи.
                val comparisonEpoch = record.epoch
                val currentPoint = calculatePoint(record.tle, comparisonEpoch)
                val radiusVector = RadiusVectorDTO(
                    x = currentPoint.r.x,
                    y = currentPoint.r.y,
                    z = currentPoint.r.z
                )

                val discrepancy = records.getOrNull(index - 1)?.let { previous ->
                    val epochDifferenceHours = Duration.between(previous.epoch, comparisonEpoch).toMillis() / MILLIS_IN_HOUR
                    val previousPointAtCurrentEpoch = calculatePoint(previous.tle, comparisonEpoch)
                    val dx = currentPoint.r.x - previousPointAtCurrentEpoch.r.x
                    val dy = currentPoint.r.y - previousPointAtCurrentEpoch.r.y
                    val dz = currentPoint.r.z - previousPointAtCurrentEpoch.r.z
                    RadiusVectorDiscrepancyDTO(
                        previousEpoch = previous.epoch,
                        epochDifferenceHours = epochDifferenceHours,
                        dx = dx,
                        dy = dy,
                        dz = dz,
                        norm = norm(dx, dy, dz)
                    )
                }

                TleAnalysisRowDTO(
                    index = index + 1,
                    epoch = record.epoch,
                    revolution = record.revolution,
                    noradId = record.noradId,
                    tleHeader = record.tle.header,
                    radiusVector = radiusVector,
                    radiusNorm = norm(radiusVector.x, radiusVector.y, radiusVector.z),
                    discrepancy = discrepancy
                )
            }
        }
        logger.info(
            "TLE analysis report built: satelliteId={}, rows={}, durationMs={}",
            satelliteId,
            rows.size,
            rowsBuildMs
        )

        return TleAnalysisResponseDTO(
            satelliteId = satelliteId,
            rows = rows
        )
    }

    private fun parseRecord(source: TLEExtensionDTO): ParsedTleRecord? = runCatching {
        val tle = source.tle
        val params = Ballistics().getTLEParams(TLE(tle.first, tle.second), tle.header ?: "")
        ParsedTleRecord(
            tle = tle,
            epoch = params.time,
            revolution = source.revolution.takeIf { it > 0 } ?: params.revolution,
            noradId = params.noradId
        )
    }.getOrNull()

    private fun calculatePoint(tle: TLEDTO, time: LocalDateTime) = runCatching {
        val stepper = TLEStepper(tle.first, tle.second, EarthType.PZ90d02)
        stepper.calculate(fromDateTime(time))
    }.getOrElse { ex ->
        throw CustomErrorException("Ошибка расчета положения по TLE на время $time: ${ex.message}")
    }

    private fun norm(x: Double, y: Double, z: Double): Double = sqrt(x * x + y * y + z * z)

    private fun SatelliteSummaryDTO.toAnalysisSatellite(
        tleSatelliteId: Long,
        recordsCount: Int
    ): TleAnalysisSatelliteDTO =
        TleAnalysisSatelliteDTO(
            id = tleSatelliteId,
            catalogId = id,
            noradId = noradId ?: tleSatelliteId,
            code = code,
            name = name,
            typeCode = typeCode,
            active = active,
            scanTle = scanTle,
            tleRecordsCount = recordsCount
        )

    private fun SatelliteSummaryDTO.toAnalysisSatelliteFallback(): TleAnalysisSatelliteDTO? {
        val analysisId = noradId ?: id.takeIf { it > 0 } ?: return null
        return TleAnalysisSatelliteDTO(
            id = analysisId,
            catalogId = id,
            noradId = noradId,
            code = code,
            name = name,
            typeCode = typeCode,
            active = active,
            scanTle = scanTle,
            tleRecordsCount = 0
        )
    }

    private data class ParsedTleRecord(
        val tle: TLEDTO,
        val epoch: LocalDateTime,
        val revolution: Long,
        val noradId: Long
    )

    companion object {
        private const val DEFAULT_TLE_ANALYSIS_COUNT = 5
        private const val MILLIS_IN_HOUR = 3_600_000.0
    }
}
