package space.nstart.pcp.slots_service.service

import ballistics.Ballistics
import ballistics.types.OrbitalPoint
import ballistics.utils.fromDateTime
import ballistics.utils.math.Vector3D
import org.springframework.stereotype.Service
import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteOrbitAvailabilityDTO
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteGroupDTO
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.groupstate.GroupStateAvailabilityDTO
import space.nstart.pcp.slots_service.dto.groupstate.GroupStateDetailsDTO
import space.nstart.pcp.slots_service.dto.groupstate.GroupStateMatrixCellDTO
import space.nstart.pcp.slots_service.dto.groupstate.GroupStateMatrixRowDTO
import space.nstart.pcp.slots_service.dto.groupstate.GroupStateSatelliteDTO
import space.nstart.pcp.slots_service.dto.groupstate.GroupStateSummaryDTO
import java.time.LocalDateTime
import kotlin.math.abs

@Service
class GroupStateService(
    private val satelliteCatalogService: SatelliteCatalogService,
    private val ballisticsService: BallisticsService
) {

    fun groupSummaries(): List<GroupStateSummaryDTO> {
        val groups = satelliteCatalogService.allGroups()
        val satellitesById = satelliteCatalogService.allSatellites().associateBy { it.id }
        val availabilityByNoradId = ballisticsService.orbitAvailability().associateBy { it.satelliteId }

        return groups.map { group ->
            GroupStateSummaryDTO(
                groupId = group.id,
                groupName = group.name,
                satelliteCount = group.satelliteIds.size,
                availableInterval = resolveAvailability(group, satellitesById, availabilityByNoradId)
            )
        }
    }

    fun groupState(groupId: Long, time: LocalDateTime?): GroupStateDetailsDTO {
        val group = satelliteCatalogService.group(groupId)
        val satellitesById = satelliteCatalogService.allSatellites().associateBy { it.id }
        val availabilityByNoradId = ballisticsService.orbitAvailability().associateBy { it.satelliteId }
        val groupSatellites = resolveGroupSatellites(group, satellitesById)
        val availableInterval = resolveAvailability(group, satellitesById, availabilityByNoradId)

        if (availableInterval == null) {
            return GroupStateDetailsDTO(
                groupId = group.id,
                groupName = group.name,
                availableInterval = null
            )
        }

        val calculationTime = time ?: availableInterval.timeStart
        if (calculationTime < availableInterval.timeStart || calculationTime > availableInterval.timeStop) {
            throw CustomErrorException(
                "Время $calculationTime не входит в доступный интервал ${availableInterval.timeStart} - ${availableInterval.timeStop}"
            )
        }

        val satellites = groupSatellites.map { satellite ->
            val noradId = satellite.id
                ?: throw CustomErrorException("Для спутника ${satellite.id} не задан NORAD ID")
            val point = ballisticsService.exactTimePoint(noradId, calculationTime)
            val keplerParams = Ballistics().calculateKeplerParams(point.toOrbitalPoint())
            GroupStateSatelliteDTO(
                satelliteId = satellite.id,
                noradId = noradId,
                code = satellite.code,
                name = satellite.name,
                omegabDeg = normalizeDegrees360(Math.toDegrees(keplerParams.omegab)),
                uDeg = normalizeDegrees360(Math.toDegrees(keplerParams.u))
            )
        }

        val matrix = satellites.map { row ->
            GroupStateMatrixRowDTO(
                satelliteId = row.satelliteId,
                cells = satellites.map { column ->
                    GroupStateMatrixCellDTO(
                        rowSatelliteId = row.satelliteId,
                        columnSatelliteId = column.satelliteId,
                        deltaOmegabDeg = normalizeDegrees180(column.omegabDeg - row.omegabDeg),
                        deltaUDeg = normalizeDegrees180(column.uDeg - row.uDeg)
                    )
                }
            )
        }

        return GroupStateDetailsDTO(
            groupId = group.id,
            groupName = group.name,
            availableInterval = availableInterval,
            calculationTime = calculationTime,
            satellites = satellites,
            matrix = matrix
        )
    }

    private fun resolveGroupSatellites(
        group: SatelliteGroupDTO,
        satellitesById: Map<Long, SatelliteSummaryDTO>
    ): List<SatelliteSummaryDTO> = group.satelliteIds.mapNotNull { satellitesById[it] }

    private fun resolveAvailability(
        group: SatelliteGroupDTO,
        satellitesById: Map<Long, SatelliteSummaryDTO>,
        availabilityByNoradId: Map<Long, SatelliteOrbitAvailabilityDTO>
    ): GroupStateAvailabilityDTO? {
        val groupSatellites = resolveGroupSatellites(group, satellitesById)
        if (group.satelliteIds.isEmpty() || groupSatellites.size != group.satelliteIds.size) {
            return null
        }

        val intervals = groupSatellites.map { satellite ->
            val noradId = satellite.id
            availabilityByNoradId[noradId] ?: return null
        }
        if (intervals.isEmpty()) {
            return null
        }

        // Общее время расчета ОГ существует только там, где пересекаются интервалы всех спутников.
        val timeStart = intervals.maxOf { it.timeStart }
        val timeStop = intervals.minOf { it.timeStop }
        return if (timeStart <= timeStop) {
            GroupStateAvailabilityDTO(timeStart = timeStart, timeStop = timeStop)
        } else {
            null
        }
    }

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

    private fun normalizeDegrees360(value: Double): Double {
        var normalized = value % 360.0
        if (normalized < 0) {
            normalized += 360.0
        }
        return normalized
    }

    private fun normalizeDegrees180(value: Double): Double {
        var normalized = normalizeDegrees360(value)
        if (normalized > 180.0) {
            normalized -= 360.0
        }
        return if (abs(normalized) < 1e-9) 0.0 else normalized
    }
}
