package space.nstart.pcp.pcp_satellite_catalog_service.service

import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import org.springframework.transaction.support.TransactionSynchronization
import org.springframework.transaction.support.TransactionSynchronizationManager
import space.nstart.pcp.pcp_satellite_catalog_service.configuration.CustomValidationException
import space.nstart.pcp.pcp_satellite_catalog_service.entity.SatelliteEntity
import space.nstart.pcp.pcp_satellite_catalog_service.entity.SatelliteGroupEntity
import space.nstart.pcp.pcp_satellite_catalog_service.entity.SatelliteGroupSatelliteEntity
import space.nstart.pcp.pcp_satellite_catalog_service.entity.SatelliteObservationProfileEntity
import space.nstart.pcp.pcp_satellite_catalog_service.entity.SatelliteSlotAngleEntity
import space.nstart.pcp.pcp_satellite_catalog_service.entity.SatelliteSlotProfileEntity
import space.nstart.pcp.pcp_satellite_catalog_service.repository.SatelliteGroupRepository
import space.nstart.pcp.pcp_satellite_catalog_service.repository.SatelliteGroupSatelliteRepository
import space.nstart.pcp.pcp_satellite_catalog_service.repository.SatelliteObservationProfileRepository
import space.nstart.pcp.pcp_satellite_catalog_service.repository.SatelliteRepository
import space.nstart.pcp.pcp_satellite_catalog_service.repository.SatelliteSlotAngleRepository
import space.nstart.pcp.pcp_satellite_catalog_service.repository.SatelliteSlotProfileRepository
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteBatchRequestDTO
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteCreateDTO
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteDeletedEventDTO
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteDTO
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteGroupCreateDTO
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteGroupDTO
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteGroupUpdateDTO
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteObservationProfileDTO
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteSlotAngleDTO
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteSlotProfileDTO
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteSummaryDTO
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteUpdateDTO

@Service
class SatelliteCatalogService(
    private val satelliteRepository: SatelliteRepository,
    private val observationProfileRepository: SatelliteObservationProfileRepository,
    private val slotProfileRepository: SatelliteSlotProfileRepository,
    private val slotAngleRepository: SatelliteSlotAngleRepository,
    private val satelliteGroupRepository: SatelliteGroupRepository,
    private val satelliteGroupSatelliteRepository: SatelliteGroupSatelliteRepository,
    private val satelliteDeletedKafkaPublisher: SatelliteDeletedKafkaPublisher
) {

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

    fun all(): List<SatelliteSummaryDTO> =
        satelliteRepository.findAllByOrderByIdAsc().map { it.toSummaryDTO() }

    fun byId(id: Long): SatelliteDTO =
        assembleSatellites(listOf(requireSatellite(id))).getValue(id)

    fun byNoradId(noradId: Long): SatelliteDTO {
        val satellite = satelliteRepository.findByNoradId(noradId)
            ?: throw CustomValidationException("Satellite with NORAD ID $noradId not found")
        return assembleSatellites(listOf(satellite)).getValue(satellite.id)
    }

    fun observationProfile(id: Long): SatelliteObservationProfileDTO {
        requireSatellite(id)
        return observationProfileRepository.findById(id).orElseThrow {
            CustomValidationException("Observation profile for satellite $id not found")
        }.toDTO()
    }

    fun slotProfile(id: Long): SatelliteSlotProfileDTO {
        requireSatellite(id)
        return assembleSlotProfiles(listOf(id))[id]
            ?: throw CustomValidationException("Slot profile for satellite $id not found")
    }

    fun batch(request: SatelliteBatchRequestDTO): List<SatelliteDTO> {
        val uniqueIds = request.ids.distinct()
        if (uniqueIds.isEmpty()) {
            return emptyList()
        }

        val satellites = satelliteRepository.findAllByIdIn(uniqueIds)
        val byId = assembleSatellites(satellites)
        return uniqueIds.map { id ->
            byId[id] ?: throw CustomValidationException("Satellite $id not found")
        }
    }

    @Transactional
    fun createSatellite(request: SatelliteCreateDTO): SatelliteDTO {
        validateSatelliteCreate(request)
        satelliteRepository.save(request.toEntity())
        return byId(request.id)
    }

    @Transactional
    fun updateSatellite(id: Long, request: SatelliteUpdateDTO): SatelliteDTO {
        requireSatellite(id)
        validateSatelliteUpdate(id, request)
        satelliteRepository.save(request.toEntity(id))
        return byId(id)
    }

    @Transactional
    fun deleteSatellite(id: Long) {
        val satellite = requireSatellite(id)
        val event = SatelliteDeletedEventDTO(
            satelliteId = satellite.id,
            noradId = satellite.noradId,
            code = satellite.code,
            name = satellite.name
        )
        satelliteRepository.deleteById(id)
        logger.info(
            "Deleted satellite from catalog: satelliteId={}, noradId={}, code={}, name={}",
            event.satelliteId,
            event.noradId,
            event.code,
            event.name
        )
        publishSatelliteDeletedAfterCommit(event)
    }

    private fun publishSatelliteDeletedAfterCommit(event: SatelliteDeletedEventDTO) {
        TransactionSynchronizationManager.registerSynchronization(
            object : TransactionSynchronization {
                override fun afterCommit() {
                    satelliteDeletedKafkaPublisher.publish(event)
                }
            }
        )
    }

    @Transactional
    fun createObservationProfile(id: Long, request: SatelliteObservationProfileDTO): SatelliteObservationProfileDTO {
        requireSatellite(id)
        if (observationProfileRepository.existsById(id)) {
            throw CustomValidationException("Observation profile for satellite $id already exists")
        }
        return observationProfileRepository.save(request.toEntity(id)).toDTO()
    }

    @Transactional
    fun updateObservationProfile(id: Long, request: SatelliteObservationProfileDTO): SatelliteObservationProfileDTO {
        requireSatellite(id)
        if (!observationProfileRepository.existsById(id)) {
            throw CustomValidationException("Observation profile for satellite $id not found")
        }
        return observationProfileRepository.save(request.toEntity(id)).toDTO()
    }

    @Transactional
    fun deleteObservationProfile(id: Long) {
        requireSatellite(id)
        if (!observationProfileRepository.existsById(id)) {
            throw CustomValidationException("Observation profile for satellite $id not found")
        }
        observationProfileRepository.deleteById(id)
    }

    @Transactional
    fun createSlotProfile(id: Long, request: SatelliteSlotProfileDTO): SatelliteSlotProfileDTO {
        requireSatellite(id)
        if (slotProfileRepository.existsById(id)) {
            throw CustomValidationException("Slot profile for satellite $id already exists")
        }
        return saveSlotProfile(id, request)
    }

    @Transactional
    fun updateSlotProfile(id: Long, request: SatelliteSlotProfileDTO): SatelliteSlotProfileDTO {
        requireSatellite(id)
        if (!slotProfileRepository.existsById(id)) {
            throw CustomValidationException("Slot profile for satellite $id not found")
        }
        return saveSlotProfile(id, request)
    }

    @Transactional
    fun deleteSlotProfile(id: Long) {
        requireSatellite(id)
        if (!slotProfileRepository.existsById(id)) {
            throw CustomValidationException("Slot profile for satellite $id not found")
        }
        slotAngleRepository.deleteAllBySatelliteId(id)
        slotProfileRepository.deleteById(id)
    }

    fun allGroups(): List<SatelliteGroupDTO> =
        assembleGroups(satelliteGroupRepository.findAllByOrderByIdAsc()).values.toList()

    fun groupById(id: Long): SatelliteGroupDTO =
        assembleGroups(listOf(requireGroup(id))).getValue(id)

    @Transactional
    fun createGroup(request: SatelliteGroupCreateDTO): SatelliteGroupDTO {
        validateGroupCreate(request)
        val satelliteIds = request.satelliteIds.distinct()
        validateSatelliteIdsExist(satelliteIds)
        val group = satelliteGroupRepository.save(SatelliteGroupEntity(name = request.name))
        saveGroupSatellites(group.id, satelliteIds)
        return groupById(group.id)
    }

    @Transactional
    fun updateGroup(id: Long, request: SatelliteGroupUpdateDTO): SatelliteGroupDTO {
        requireGroup(id)
        validateGroupUpdate(id, request)
        val satelliteIds = request.satelliteIds.distinct()
        validateSatelliteIdsExist(satelliteIds)
        satelliteGroupRepository.save(SatelliteGroupEntity(id = id, name = request.name))
        satelliteGroupSatelliteRepository.deleteAllByGroupId(id)
        saveGroupSatellites(id, satelliteIds)
        return groupById(id)
    }

    @Transactional
    fun deleteGroup(id: Long) {
        requireGroup(id)
        satelliteGroupRepository.deleteById(id)
    }

    private fun saveSlotProfile(id: Long, request: SatelliteSlotProfileDTO): SatelliteSlotProfileDTO {
        slotProfileRepository.save(request.toEntity(id))
        slotAngleRepository.deleteAllBySatelliteId(id)
        slotAngleRepository.saveAll(
            request.defaultAngles.mapIndexed { index, angle ->
                SatelliteSlotAngleEntity(
                    satelliteId = id,
                    position = index + 1,
                    angleBegin = angle.angleBegin,
                    angleEnd = angle.angleEnd
                )
            }
        )
        return assembleSlotProfiles(listOf(id)).getValue(id)
    }

    private fun saveGroupSatellites(groupId: Long, satelliteIds: List<Long>) {
        if (satelliteIds.isEmpty()) {
            return
        }
        satelliteGroupSatelliteRepository.saveAll(
            satelliteIds.map { satelliteId ->
                SatelliteGroupSatelliteEntity(
                    groupId = groupId,
                    satelliteId = satelliteId
                )
            }
        )
    }

    private fun validateSatelliteCreate(request: SatelliteCreateDTO) {
        if (satelliteRepository.existsById(request.id)) {
            throw CustomValidationException("Satellite ${request.id} already exists")
        }
        if (satelliteRepository.existsByCode(request.code)) {
            throw CustomValidationException("Satellite code ${request.code} already exists")
        }
        request.noradId?.let { noradId ->
            if (satelliteRepository.existsByNoradId(noradId)) {
                throw CustomValidationException("Satellite with NORAD ID $noradId already exists")
            }
        }
    }

    private fun validateSatelliteUpdate(id: Long, request: SatelliteUpdateDTO) {
        if (satelliteRepository.existsByCodeAndIdNot(request.code, id)) {
            throw CustomValidationException("Satellite code ${request.code} already exists")
        }
        request.noradId?.let { noradId ->
            if (satelliteRepository.existsByNoradIdAndIdNot(noradId, id)) {
                throw CustomValidationException("Satellite with NORAD ID $noradId already exists")
            }
        }
    }

    private fun validateGroupCreate(request: SatelliteGroupCreateDTO) {
        if (satelliteGroupRepository.existsByName(request.name)) {
            throw CustomValidationException("Satellite group ${request.name} already exists")
        }
    }

    private fun validateGroupUpdate(id: Long, request: SatelliteGroupUpdateDTO) {
        if (satelliteGroupRepository.existsByNameAndIdNot(request.name, id)) {
            throw CustomValidationException("Satellite group ${request.name} already exists")
        }
    }

    private fun validateSatelliteIdsExist(ids: List<Long>) {
        if (ids.isEmpty()) {
            return
        }
        val existingIds = satelliteRepository.findAllByIdIn(ids).map { it.id }.toSet()
        val missingIds = ids.filterNot(existingIds::contains)
        if (missingIds.isNotEmpty()) {
            throw CustomValidationException("Satellites not found: ${missingIds.joinToString(", ")}")
        }
    }

    private fun requireSatellite(id: Long): SatelliteEntity =
        satelliteRepository.findById(id).orElseThrow {
            CustomValidationException("Satellite $id not found")
        }

    private fun requireGroup(id: Long): SatelliteGroupEntity =
        satelliteGroupRepository.findById(id).orElseThrow {
            CustomValidationException("Satellite group $id not found")
        }

    private fun assembleSatellites(satellites: List<SatelliteEntity>): Map<Long, SatelliteDTO> {
        val ids = satellites.map { it.id }
        val observationProfiles = observationProfileRepository.findAllBySatelliteIdIn(ids)
            .associateBy { it.satelliteId }
        val slotProfiles = assembleSlotProfiles(ids)

        return satellites.associate { satellite ->
            val observationProfile = observationProfiles[satellite.id]?.toDTO()
            satellite.id to SatelliteDTO(
                id = satellite.id,
                noradId = satellite.noradId,
                code = satellite.code,
                name = satellite.name,
                typeCode = satellite.typeCode,
                active = satellite.active,
                scanTle = satellite.scanTle,
                visualization = satellite.toSummaryDTO().visualization,
                observationProfile = observationProfile,
                slotProfile = slotProfiles[satellite.id]
            )
        }
    }

    private fun assembleSlotProfiles(ids: List<Long>): Map<Long, SatelliteSlotProfileDTO> {
        val slotProfiles = slotProfileRepository.findAllBySatelliteIdIn(ids)
            .associateBy { it.satelliteId }
        val anglesBySatelliteId = slotAngleRepository.findAllBySatelliteIdInOrderBySatelliteIdAscPositionAsc(ids)
            .groupBy({ it.satelliteId }) {
                SatelliteSlotAngleDTO(
                    angleBegin = it.angleBegin,
                    angleEnd = it.angleEnd
                )
            }

        return slotProfiles.mapValues { (_, profile) ->
            SatelliteSlotProfileDTO(
                cycleRevs = profile.cycleRevs,
                tnCalc = profile.tnCalc,
                slotDuration = profile.slotDuration,
                durationCalcDays = profile.durationCalcDays,
                maxChainLength = profile.maxChainLength,
                defaultAngles = anglesBySatelliteId[profile.satelliteId].orEmpty()
            )
        }
    }

    private fun assembleGroups(groups: List<SatelliteGroupEntity>): Map<Long, SatelliteGroupDTO> {
        val groupIds = groups.map { it.id }
        val memberships = satelliteGroupSatelliteRepository.findAllByGroupIdInOrderByGroupIdAscSatelliteIdAsc(groupIds)
            .groupBy({ it.groupId }, { it.satelliteId })

        return groups.associate { group ->
            group.id to SatelliteGroupDTO(
                id = group.id,
                name = group.name,
                satelliteIds = memberships[group.id].orEmpty()
            )
        }
    }

    private fun SatelliteCreateDTO.toEntity() = SatelliteEntity(
        id = id,
        noradId = noradId,
        code = code,
        name = name,
        typeCode = typeCode,
        active = active,
        scanTle = scanTle,
        red = visualization.red,
        green = visualization.green,
        blue = visualization.blue
    )

    private fun SatelliteUpdateDTO.toEntity(id: Long) = SatelliteEntity(
        id = id,
        noradId = noradId,
        code = code,
        name = name,
        typeCode = typeCode,
        active = active,
        scanTle = scanTle,
        red = visualization.red,
        green = visualization.green,
        blue = visualization.blue
    )

    private fun SatelliteObservationProfileDTO.toEntity(satelliteId: Long) = SatelliteObservationProfileEntity(
        satelliteId = satelliteId,
        captureAngle = captureAngle,
        sunAngleMin = sunAngleMin,
        durationMinSeconds = durationMinSeconds,
        durationMaxSeconds = durationMaxSeconds,
        mmiSeconds = mmiSeconds,
        dailyMaxDurationSeconds = dailyMaxDurationSeconds,
        revolutionMaxDurationSeconds = revolutionMaxDurationSeconds
    )

    private fun SatelliteSlotProfileDTO.toEntity(satelliteId: Long) = SatelliteSlotProfileEntity(
        satelliteId = satelliteId,
        cycleRevs = cycleRevs,
        tnCalc = tnCalc,
        slotDuration = slotDuration,
        durationCalcDays = durationCalcDays,
        maxChainLength = maxChainLength
    )
}
