package org.nstart.dep265.requestservice.service

import org.nstart.dep265.requestservice.entity.EarthCellEntity
import org.nstart.dep265.requestservice.entity.EarthGridSettingsEntity
import org.nstart.dep265.requestservice.repository.EarthCellRepository
import org.nstart.dep265.requestservice.repository.EarthGridSettingsRepository
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import space.nstart.pcp.pcp_types_lib.dto.requests.EarthCellDTO
import space.nstart.pcp.pcp_types_lib.dto.requests.EarthCellWithRequestsDTO
import kotlin.math.PI
import kotlin.math.floor
import kotlin.math.truncate

@Service
class EarthGridCatalogService(
    private val earthCellRepository: EarthCellRepository,

    private val earthGridSettingsRepository: EarthGridSettingsRepository,

    private val compatGeometryService: CompatGeometryService,

    @Value("\${app.grid.enabled:true}")
    private val gridEnabled: Boolean,
) {
    private data class AggregatedGridKey(
        val latitudeGroup: Long,
        val longitudeGroup: Long,
    )

    @Transactional(readOnly = true)
    fun all(countLat: Int? = null, countLong: Int? = null): List<EarthCellDTO> {
        val earthCells = earthCellRepository.findAll()
        return aggregateIfRequested(earthCells, countLat, countLong)
    }

    @Transactional(readOnly = true)
    fun paintByImportance(importance: Double): String {
        val earthCells = earthCellRepository.findAllByImportanceGreaterThanOrderByImportanceDesc(importance)
        /** Контуры ячеек складываются в GEOMETRYCOLLECTION для legacy paint endpoint. */
        val geometryCollection = compatGeometryService.toGeometryCollection(earthCells.map { earthCell -> earthCell.contour })
        return geometryCollection
    }

    @Transactional(readOnly = true)
    fun byImportance(
        importance: Double,
        countLat: Int? = null,
        countLong: Int? = null,
    ): List<EarthCellDTO> {
        val earthCells = earthCellRepository.findAllByImportanceGreaterThanOrderByImportanceDesc(importance)
        return aggregateIfRequested(earthCells, countLat, countLong)
    }

    @Transactional(readOnly = true)
    fun allWithRequests(
        minImportance: Double? = null,
        countLat: Int? = null,
        countLong: Int? = null,
    ): List<EarthCellWithRequestsDTO> {
        val earthCells = earthCellRepository.findCellsWithRequestProjections(minImportance)
        return aggregateWithRequestsIfRequested(earthCells, countLat, countLong)
    }

    @Transactional(readOnly = true)
    fun byId(cellId: Long) = earthCellRepository.findWithRequestProjectionsByCellId(cellId)
        .map { earthCell -> earthCell.toDtoWithRequests() }

    @Transactional(readOnly = true)
    fun byNum(cellNum: Long) = earthCellRepository.findWithRequestProjectionsByCellNum(cellNum)
        .map { earthCell -> earthCell.toDtoWithRequests() }

    @Transactional(readOnly = true)
    fun byNumForPaint(cellNum: Long): String {
        val earthCell = earthCellRepository.findWithRequestProjectionsByCellNum(cellNum).orElse(null) ?: return ""

        /** Контур самой ячейки должен идти первым, как это делал старый earth-grid. */
        val wkts = mutableListOf<String>()
        wkts.add(earthCell.contour)
        wkts.addAll(earthCell.requestProjections.map { requestProjection -> requestProjection.contour })

        val geometryCollection = compatGeometryService.toGeometryCollection(wkts)
        return geometryCollection
    }

    @Transactional
    fun init(step: Double) {
        require(step > 0.0) { "Шаг сетки должен быть больше 0" }
        updateCalculationStep(step)

        if (!gridEnabled) {
            return
        }

        /** Все старые ячейки удаляются, потому что init — это полный rebuild каталога. */
        earthCellRepository.deleteAllInBatch()

        /** Текущая широта стартует по правилам old earth-grid сервиса. */
        var latitude = -90 + step
        while (latitude <= 90) {
            val line = mutableListOf<EarthCellEntity>()

            var longitude = 0.0
            while (longitude < (360-0.001)) {
                /** Номер ячейки вычисляется legacy-формулой через индексы b/l. */
                val cellNum = getInd(latitude * PI / 180.0, longitude * PI / 180.0)

                /** Контур повторяет старую геометрию earth-grid 1x1 градус. */
                val contour = "POLYGON (($longitude $latitude, ${longitude - step} $latitude, ${longitude - step} ${latitude - step}, $longitude ${latitude - step}, $longitude $latitude))"

                val earthCell = EarthCellEntity(
                    cellId = null,
                    cellNum = cellNum,
                    longitude = longitude,
                    latitude = latitude,
                    importance = 0.0,
                    contour = contour,
                )
                line.add(earthCell)
                longitude += step
            }

            earthCellRepository.saveAll(line)
            latitude += step
        }
    }

    private fun updateCalculationStep(step: Double) {
        val settings = earthGridSettingsRepository.findById(EarthGridSettingsEntity.DEFAULT_SETTINGS_ID)
            .orElseGet {
                EarthGridSettingsEntity(
                    settingsId = EarthGridSettingsEntity.DEFAULT_SETTINGS_ID,
                    calculationStep = EarthGridSettingsEntity.DEFAULT_CALCULATION_STEP,
                )
            }
        settings.calculationStep = step
        earthGridSettingsRepository.save(settings)
    }

    private fun aggregateIfRequested(
        earthCells: List<EarthCellEntity>,
        countLat: Int?,
        countLong: Int?,
    ): List<EarthCellDTO> {
        if (countLat == null && countLong == null) {
            return earthCells.map { earthCell -> earthCell.toDto() }
        }

        val latitudeCount = countLat ?: 1
        val longitudeCount = countLong ?: 1
        require(latitudeCount > 0) { "Параметр countLat должен быть больше 0" }
        require(longitudeCount > 0) { "Параметр countLong должен быть больше 0" }

        val calculationStep = currentCalculationStep()
        return earthCells
            .groupBy { earthCell -> aggregationKey(earthCell, calculationStep, latitudeCount, longitudeCount) }
            .values
            .map { group -> aggregateCells(group, calculationStep) }
            .sortedWith(compareByDescending<EarthCellDTO> { it.importance }.thenBy { it.num })
    }

    private fun aggregateWithRequestsIfRequested(
        earthCells: List<EarthCellEntity>,
        countLat: Int?,
        countLong: Int?,
    ): List<EarthCellWithRequestsDTO> {
        if (countLat == null && countLong == null) {
            return earthCells.map { earthCell -> earthCell.toDtoWithRequests() }
        }

        val latitudeCount = countLat ?: 1
        val longitudeCount = countLong ?: 1
        require(latitudeCount > 0) { "Параметр countLat должен быть больше 0" }
        require(longitudeCount > 0) { "Параметр countLong должен быть больше 0" }

        val calculationStep = currentCalculationStep()
        return earthCells
            .groupBy { earthCell -> aggregationKey(earthCell, calculationStep, latitudeCount, longitudeCount) }
            .values
            .map { group -> aggregateCellsWithRequests(group, calculationStep) }
            .sortedWith(compareByDescending<EarthCellWithRequestsDTO> { it.importance }.thenBy { it.num })
    }

    private fun currentCalculationStep(): Double =
        earthGridSettingsRepository.findById(EarthGridSettingsEntity.DEFAULT_SETTINGS_ID)
            .map { settings -> settings.calculationStep }
            .orElse(EarthGridSettingsEntity.DEFAULT_CALCULATION_STEP)

    private fun aggregationKey(
        earthCell: EarthCellEntity,
        calculationStep: Double,
        countLat: Int,
        countLong: Int,
    ): AggregatedGridKey {
        val latitude = earthCell.latitude ?: 0.0
        val longitude = earthCell.longitude ?: 0.0
        val latitudeIndex = floor((latitude - (-90.0 + calculationStep)) / calculationStep).toLong().coerceAtLeast(0L)
        val longitudeIndex = floor(longitude / calculationStep).toLong().coerceAtLeast(0L)
        return AggregatedGridKey(
            latitudeGroup = latitudeIndex / countLat,
            longitudeGroup = longitudeIndex / countLong,
        )
    }

    private fun aggregateCells(
        earthCells: List<EarthCellEntity>,
        calculationStep: Double,
    ): EarthCellDTO {
        val minLongitude = earthCells.minOf { earthCell -> (earthCell.longitude ?: 0.0) - calculationStep }
        val maxLongitude = earthCells.maxOf { earthCell -> earthCell.longitude ?: 0.0 }
        val minLatitude = earthCells.minOf { earthCell -> (earthCell.latitude ?: 0.0) - calculationStep }
        val maxLatitude = earthCells.maxOf { earthCell -> earthCell.latitude ?: 0.0 }
        val importance = earthCells.sumOf { earthCell -> earthCell.importance }
        val firstCell = earthCells.minWith(compareBy<EarthCellEntity> { it.latitude ?: 0.0 }.thenBy { it.longitude ?: 0.0 })
        val contour = "POLYGON (($minLongitude $minLatitude, $maxLongitude $minLatitude, $maxLongitude $maxLatitude, $minLongitude $maxLatitude, $minLongitude $minLatitude))"

        return EarthCellDTO(
            id = firstCell.cellId ?: firstCell.cellNum,
            num = firstCell.cellNum,
            latitude = (minLatitude + maxLatitude) / 2.0,
            longitude = (minLongitude + maxLongitude) / 2.0,
            importance = importance,
            contour = contour,
        )
    }

    private fun aggregateCellsWithRequests(
        earthCells: List<EarthCellEntity>,
        calculationStep: Double,
    ): EarthCellWithRequestsDTO {
        val aggregate = aggregateCells(earthCells, calculationStep)
        return EarthCellWithRequestsDTO(
            id = aggregate.id,
            num = aggregate.num,
            latitude = aggregate.latitude,
            longitude = aggregate.longitude,
            importance = aggregate.importance,
            contour = aggregate.contour,
            requests = earthCells.flatMap { earthCell -> earthCell.requestProjections.map { it.toDto() } },
        )
    }

    fun getBInd(latitudeRadians: Double): Long {
        var latitudeDegrees = latitudeRadians * 180.0 / PI
        latitudeDegrees = 90.0 - latitudeDegrees
        var index = truncate(latitudeDegrees).toLong()
        if (index > 180) {
            index = 180
        }
        return index
    }

    fun getLInd(longitudeRadians: Double): Long {
        var longitudeDegrees = longitudeRadians * 180.0 / PI
        if (longitudeDegrees < 0) {
            longitudeDegrees += 360.0
        }
        var index = truncate(longitudeDegrees).toLong()
        if (index > 359) {
            index = 359
        }
        return index
    }

    fun getInd(latitudeRadians: Double, longitudeRadians: Double): Long {
        /** Итоговый номер повторяет алгоритм old earth-grid сервиса. */
        val cellNum = getBInd(latitudeRadians) * 360 + getLInd(longitudeRadians)
        return cellNum
    }
}
