package space.nstart.pcp.pcp_coverage_scheme_service.service

import org.locationtech.jts.geom.Coordinate
import org.locationtech.jts.geom.Geometry
import org.locationtech.jts.geom.GeometryCollection
import org.locationtech.jts.geom.GeometryFactory
import org.locationtech.jts.geom.MultiPolygon
import org.locationtech.jts.geom.Polygon
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import space.nstart.pcp.pcp_coverage_scheme_service.client.CoverageBallisticsClient
import space.nstart.pcp.pcp_coverage_scheme_service.configuration.CoverageSchemeProperties
import space.nstart.pcp.pcp_coverage_scheme_service.configuration.CustomValidationException
import space.nstart.pcp.pcp_types_lib.dto.ballistics.FlightLineDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.ObjDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.ObjViewRequestDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SquareViewParamDTO
import space.nstart.pcp.pcp_types_lib.dto.coverage.CoverageObservationDTO
import space.nstart.pcp.pcp_types_lib.dto.coverage.CoverageSchemeCalculateRequestDTO
import space.nstart.pcp.pcp_types_lib.dto.coverage.CoverageSchemeResponseDTO

@Service
class CoverageSchemeCalculationService(
    private val ballisticsClient: CoverageBallisticsClient,
    private val coverageSchemeBuilderService: CoverageSchemeBuilderService,
    private val geometrySupport: GeometrySupport,
    private val properties: CoverageSchemeProperties
) {

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

    fun calculate(request: CoverageSchemeCalculateRequestDTO): CoverageSchemeResponseDTO {
        validateRequest(request)
        val longitudeModel = geometrySupport.resolveLongitudeModel(request.polygonWkt)
        val target = geometrySupport.parseAndNormalizeTarget(request.polygonWkt, longitudeModel)
        val views = ballisticsClient.mplSquare(
            ObjViewRequestDTO(
                satellites = request.satelliteIds,
                timeStart = request.timeStart,
                timeStop = request.timeEnd,
                obj = ObjDTO(
                    id = "coverage-scheme-target",
                    contourWKT = request.polygonWkt
                ),
                sunAngleMin = request.sunAngle
            )
        )
            .sortedWith(
                compareBy<SquareViewParamDTO>(
                    { it.noradId },
                    { it.timeBegin },
                    { it.timeEnd },
                    { it.revolutionBegin },
                    { it.revolutionEnd },
                    { it.gammaMin },
                    { it.gammaMax }
                )
            )

        logger.info("Получено ${views.size} моментов наблюдений")

        val observations = views
            .map { observation ->
                CoverageObservationDTO(
                    observation = observation,
                    intersectionContourWkts = buildIntersectionContourWkts(target, observation, longitudeModel)
                )
            }
        val coverageScheme = coverageSchemeBuilderService.buildCoverageScheme(
            observations = observations,
            rollStepDegrees = request.rollStepDegrees,
            longitudeModel = longitudeModel
        )

        logger.info(
            "Coverage scheme calculated: observations={}, slots={}, satellites={}",
            observations.size,
            coverageScheme.size,
            request.satelliteIds.size
        )

        return CoverageSchemeResponseDTO(
            targetPolygonWkt = geometrySupport.toWkt(target),
            observations = observations,
            coverageScheme = coverageScheme
        )
    }

    private fun buildIntersectionContourWkts(
        target: Geometry,
        observation: SquareViewParamDTO,
        longitudeModel: GeometrySupport.LongitudeModel
    ): List<String> {
        val paddedStart = observation.timeBegin.minusSeconds(properties.flightLinePaddingSeconds)
        val paddedEnd = observation.timeEnd.plusSeconds(properties.flightLinePaddingSeconds)
        val flightLine = ballisticsClient.flightLine(observation.noradId, paddedStart, paddedEnd)
            .map { normalizeFlightLine(it, longitudeModel) }
            .sortedBy { it.time }

        if (flightLine.size < 2) {
            logger.debug(
                "Skip strip intersection because flight line is too short: satelliteId={}, interval=[{} - {}], points={}",
                observation.noradId,
                observation.timeBegin,
                observation.timeEnd,
                flightLine.size
            )
            return emptyList()
        }

        val strip = buildStripGeometry(target, flightLine)
        if (strip.isEmpty) {
            return emptyList()
        }

        val intersection = geometrySupport.safeIntersection(strip, target)
        return extractPolygons(intersection)
            .map(geometrySupport::toWkt)
            .filter { it.isNotBlank() }
    }

    private fun normalizeFlightLine(
        point: FlightLineDTO,
        longitudeModel: GeometrySupport.LongitudeModel
    ) = FlightLineDTO(
        time = point.time,
        revolution = point.revolution,
        lat = point.lat,
        long = normalizeLongitude(point.long, longitudeModel),
        latLeft = point.latLeft,
        longLeft = normalizeLongitude(point.longLeft, longitudeModel),
        latInnerLeft = point.latInnerLeft,
        longInnerLeft = normalizeLongitude(point.longInnerLeft, longitudeModel),
        latInnerRight = point.latInnerRight,
        longInnerRight = normalizeLongitude(point.longInnerRight, longitudeModel),
        latRight = point.latRight,
        longRight = normalizeLongitude(point.longRight, longitudeModel),
        revSign = point.revSign
    )

    private fun normalizeLongitude(
        longitude: Double,
        longitudeModel: GeometrySupport.LongitudeModel
    ): Double = when (longitudeModel) {
        GeometrySupport.LongitudeModel.STANDARD_180 -> normalizeToStandardBand(longitude)
        GeometrySupport.LongitudeModel.POSITIVE_360 -> normalizeToPositiveBand(longitude)
    }

    private fun normalizeToStandardBand(longitude: Double): Double {
        var normalized = longitude % 360.0
        if (normalized <= -180.0) {
            normalized += 360.0
        }
        if (normalized > 180.0) {
            normalized -= 360.0
        }
        return normalized
    }

    private fun normalizeToPositiveBand(longitude: Double): Double {
        var normalized = longitude % 360.0
        if (normalized < 0.0) {
            normalized += 360.0
        }
        return normalized
    }

    private fun buildStripGeometry(target: Geometry, flightLine: List<FlightLineDTO>): Geometry {
        val outerLeftBoundary = geometrySupport.alignTrackCoordinatesToTarget(
            flightLine.map { Coordinate(it.longLeft, it.latLeft, 0.0) },
            target
        )
        val outerRightBoundary = geometrySupport.alignTrackCoordinatesToTarget(
            flightLine.map { Coordinate(it.longRight, it.latRight, 0.0) },
            target
        )

        if (outerLeftBoundary.size < 2 || outerRightBoundary.size < 2) {
            return geometryFactory.createPolygon()
        }

        val innerLeftBoundary = geometrySupport.alignTrackCoordinatesToTarget(
            flightLine.map { Coordinate(it.longInnerLeft, it.latInnerLeft, 0.0) },
            target
        )
        val innerRightBoundary = geometrySupport.alignTrackCoordinatesToTarget(
            flightLine.map { Coordinate(it.longInnerRight, it.latInnerRight, 0.0) },
            target
        )

        val geometries = listOfNotNull(
            buildRibbonPolygon(outerLeftBoundary, innerLeftBoundary, flightLine),
            buildRibbonPolygon(innerRightBoundary, outerRightBoundary, flightLine)
        )
        if (geometries.isEmpty()) {
            return geometryFactory.createPolygon()
        }
        return geometries.drop(1).fold<Polygon, Geometry>(geometries.first()) { merged, geometry ->
            geometrySupport.safeUnion(merged, geometry)
        }
    }

    private fun buildRibbonPolygon(
        leftBoundary: List<Coordinate>,
        rightBoundary: List<Coordinate>,
        flightLine: List<FlightLineDTO>
    ): Polygon? {
        val shell = toRingCoordinates(leftBoundary, rightBoundary) ?: return null
        return runCatching {
            geometryFactory.createPolygon(shell.toTypedArray())
        }.getOrElse { error ->
            logger.warn(
                "Failed to build strip segment polygon: firstPointTime={}, points={}, error={}",
                flightLine.firstOrNull()?.time,
                flightLine.size,
                error.message
            )
            null
        }
    }

    private fun toRingCoordinates(leftBoundary: List<Coordinate>, rightBoundary: List<Coordinate>): List<Coordinate>? {
        if (leftBoundary.size < 2 || rightBoundary.size < 2) {
            return null
        }
        val ring = ArrayList<Coordinate>(leftBoundary.size + rightBoundary.size + 1)
        leftBoundary.forEach { ring += Coordinate(it) }
        rightBoundary.asReversed().forEach { ring += Coordinate(it) }
        if (ring.size < 4) {
            return null
        }
        ring += Coordinate(ring.first())

        val distinctVertices = ring
            .dropLast(1)
            .map { it.x to it.y }
            .distinct()
        return ring.takeIf { distinctVertices.size >= 3 }
    }

    private fun extractPolygons(geometry: Geometry): List<Polygon> {
        if (geometry.isEmpty) return emptyList()
        return when (geometry) {
            is Polygon -> listOf(geometry)
            is MultiPolygon -> (0 until geometry.numGeometries)
                .map { geometry.getGeometryN(it) as Polygon }
            is GeometryCollection -> (0 until geometry.numGeometries)
                .flatMap { extractPolygons(geometry.getGeometryN(it)) }
            else -> emptyList()
        }
    }

    private fun validateRequest(request: CoverageSchemeCalculateRequestDTO) {
        val minimumTechnologyOverlap = request.minimumTechnologyOverlap
        if (request.satelliteIds.isEmpty()) {
            throw CustomValidationException("satelliteIds must not be empty")
        }
        if (!request.timeStart.isBefore(request.timeEnd)) {
            throw CustomValidationException("timeStart must be before timeEnd")
        }
        if (minimumTechnologyOverlap != null && minimumTechnologyOverlap < 0.0) {
            throw CustomValidationException("minimumTechnologyOverlap must be non-negative")
        }
    }
}
