package space.nstart.pcp.routepassportconsumer.service

import org.locationtech.jts.geom.Coordinate
import org.locationtech.jts.geom.Geometry
import org.locationtech.jts.geom.GeometryFactory
import org.locationtech.jts.geom.LinearRing
import org.locationtech.jts.geom.Polygon
import org.locationtech.jts.io.WKTWriter
import org.springframework.stereotype.Service
import space.nstart.pcp.pcp_types_lib.dto.routes.RawGeoJsonGeometryDto
import space.nstart.pcp.routepassportconsumer.exception.RoutePassportContractException
import tools.jackson.databind.JsonNode

/**
 * Конвертирует поддерживаемую GeoJSON geometry входного паспорта маршрута в WKT.
 */
@Service
open class RouteGeometryConverter {
    private val geometryFactory = GeometryFactory()
    private val wktWriter = WKTWriter()

    open fun convert(geometry: RawGeoJsonGeometryDto?): String {
        if (geometry == null) {
            throw RoutePassportContractException("Отсутствует обязательное поле geometry")
        }

        val jtsGeometry = when (geometry.type) {
            "Polygon" -> toPolygon(geometry.coordinates, fieldName = "geometry.coordinates")
            "MultiPolygon" -> toMultiPolygon(geometry.coordinates)
            else -> throw RoutePassportContractException("Неподдерживаемый тип geometry: ${geometry.type}")
        }

        return writeCompactWkt(jtsGeometry)
    }

    private fun toMultiPolygon(coordinatesNode: JsonNode): Geometry {
        requireArray(coordinatesNode, "geometry.coordinates")

        if (coordinatesNode.size() == 0) {
            throw RoutePassportContractException("MultiPolygon geometry.coordinates не должен быть пустым")
        }

        val polygons = coordinatesNode.mapIndexed { index, polygonNode ->
            toPolygon(polygonNode, fieldName = "geometry.coordinates[$index]")
        }.toTypedArray()

        return geometryFactory.createMultiPolygon(polygons)
    }

    private fun toPolygon(
        coordinatesNode: JsonNode,
        fieldName: String,
    ): Polygon {
        requireArray(coordinatesNode, fieldName)

        if (coordinatesNode.size() == 0) {
            throw RoutePassportContractException("Polygon $fieldName должен содержать хотя бы один linear ring")
        }

        val shell = toLinearRing(coordinatesNode[0], "$fieldName[0]")
        val holes = coordinatesNode.drop(1).mapIndexed { index, ringNode ->
            toLinearRing(ringNode, "$fieldName[${index + 1}]")
        }.toTypedArray()

        return try {
            geometryFactory.createPolygon(shell, holes)
        } catch (exception: IllegalArgumentException) {
            throw RoutePassportContractException("Некорректная структура Polygon $fieldName", exception)
        }
    }

    private fun toLinearRing(
        ringNode: JsonNode,
        fieldName: String,
    ): LinearRing {
        requireArray(ringNode, fieldName)

        if (ringNode.size() < MIN_LINEAR_RING_POINTS) {
            throw RoutePassportContractException(
                "Linear ring $fieldName должен содержать минимум $MIN_LINEAR_RING_POINTS точки",
            )
        }

        val coordinates = ringNode.mapIndexed { index, coordinateNode ->
            toCoordinate(coordinateNode, "$fieldName[$index]")
        }.toTypedArray()

        if (!coordinates.first().equals2D(coordinates.last())) {
            throw RoutePassportContractException("Linear ring $fieldName должен быть замкнут")
        }

        return try {
            geometryFactory.createLinearRing(coordinates)
        } catch (exception: IllegalArgumentException) {
            throw RoutePassportContractException("Некорректная структура linear ring $fieldName", exception)
        }
    }

    private fun toCoordinate(
        coordinateNode: JsonNode,
        fieldName: String,
    ): Coordinate {
        requireArray(coordinateNode, fieldName)

        if (coordinateNode.size() < MIN_COORDINATE_VALUES) {
            throw RoutePassportContractException("Точка $fieldName должна содержать минимум две координаты")
        }

        if (!coordinateNode[0].isNumber || !coordinateNode[1].isNumber) {
            throw RoutePassportContractException("Точка $fieldName должна содержать числовые longitude и latitude")
        }

        return try {
            Coordinate(
                coordinateNode[0].decimalValue().toDouble(),
                coordinateNode[1].decimalValue().toDouble(),
            )
        } catch (exception: RuntimeException) {
            throw RoutePassportContractException("Точка $fieldName содержит некорректные координаты", exception)
        }
    }

    private fun requireArray(
        node: JsonNode,
        fieldName: String,
    ) {
        if (!node.isArray) {
            throw RoutePassportContractException("Поле $fieldName должно быть массивом GeoJSON")
        }
    }

    private fun writeCompactWkt(geometry: Geometry): String =
        when (geometry.geometryType) {
            "Polygon" -> wktWriter.write(geometry).removePrefix("POLYGON ").let { "POLYGON$it" }
            "MultiPolygon" -> wktWriter.write(geometry).removePrefix("MULTIPOLYGON ").let { "MULTIPOLYGON$it" }
            else -> wktWriter.write(geometry)
        }

    private companion object {
        const val MIN_COORDINATE_VALUES = 2
        const val MIN_LINEAR_RING_POINTS = 4
    }
}
