package space.nstart.pcp.pcp_stations_service.integration

import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonProperty
import org.slf4j.LoggerFactory
import org.springframework.http.MediaType
import org.springframework.stereotype.Component
import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.reactive.function.client.WebClientRequestException
import org.springframework.web.reactive.function.client.WebClientResponseException
import space.nstart.pcp.pcp_stations_service.configuration.ClassifierProperties
import space.nstart.pcp.pcp_types_lib.dto.ballistics.PositionDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.StationDTO
import java.time.Duration
import java.util.UUID

@Component
class NsiStationClient(
    webClientBuilder: WebClient.Builder,
    private val classifierProperties: ClassifierProperties,
) {
    private val log = LoggerFactory.getLogger(javaClass)

    private val webClient: WebClient = webClientBuilder.build()

    private fun classifyResponseException(
        ex: WebClientResponseException
    ): NsiStationsException {
        val statusCode = ex.statusCode.value()

        return if (
            statusCode == 408 ||
            statusCode == 429 ||
            statusCode in 500..599
        ) {
            NsiStationsUnavailableException(
                classifierUrl = classifierProperties.stationsUrl,
                statusCode = statusCode,
                cause = ex
            )
        } else {
            NsiStationsResponseException(
                classifierUrl = classifierProperties.stationsUrl,
                statusCode = statusCode,
                cause = ex
            )
        }
    }

    fun fetchStations(): List<StationDTO> {
        log.info(
            "Fetching stations from NSI. url={}",
            classifierProperties.stationsUrl
        )

        val response: List<NsiStationRecord> = try {
            webClient.get()
                .uri(classifierProperties.stationsUrl)
                .accept(MediaType.APPLICATION_JSON)
                .retrieve()
                .bodyToMono(Array<NsiStationRecord>::class.java)
                .block(Duration.ofSeconds(10))
                ?.toList()
                ?: throw NsiStationsUnavailableException(
                    classifierUrl = classifierProperties.stationsUrl,
                    statusCode = null,
                    cause = IllegalStateException("NSI response body is absent")
                )
        } catch (ex: NsiStationsException) {
            throw ex
        } catch (ex: WebClientResponseException) {
            throw classifyResponseException(ex)
        } catch (ex: WebClientRequestException) {
            throw NsiStationsUnavailableException(
                classifierUrl = classifierProperties.stationsUrl,
                statusCode = null,
                cause = ex
            )
        } catch (ex: IllegalStateException) {
            /*
             * Сюда, в частности, попадает timeout от block(Duration).
             */
            throw NsiStationsUnavailableException(
                classifierUrl = classifierProperties.stationsUrl,
                statusCode = null,
                cause = ex
            )
        }

        return response.mapIndexed { index, station ->
            station.toStationDTO(number = index + 1)
        }
    }

    private fun NsiStationRecord.toStationDTO(number: Int): StationDTO {
        return StationDTO(
            id = id.toUuidOrNull(),
            number = number,
            name = data?.name ?: businessKey ?: "",
            position = PositionDTO(
                lat = data?.lat ?: 0.0,
                long = data?.lon ?: 0.0,
                height = data?.elevationM ?: 0.0
            ),
            elevationMin = data?.elevationMinDeg ?: 0.0,
            elevationMax = 90.0
        )
    }

    private fun String?.toUuidOrNull(): UUID? =
        this?.let {
            runCatching { UUID.fromString(it) }.getOrNull()
        }
}

sealed class NsiStationsException(
    message: String,
    cause: Throwable?
) : RuntimeException(message, cause)

class NsiStationsUnavailableException(
    val classifierUrl: String,
    val statusCode: Int?,
    cause: Throwable?
) : NsiStationsException(
    message = "NSI stations classifier is temporarily unavailable: $classifierUrl",
    cause = cause
)

class NsiStationsResponseException(
    val classifierUrl: String,
    val statusCode: Int,
    cause: Throwable?
) : NsiStationsException(
    message = "NSI stations classifier returned HTTP $statusCode: $classifierUrl",
    cause = cause
)

@JsonIgnoreProperties(ignoreUnknown = true)
data class NsiStationRecord(
    val id: String? = null,
    val businessKey: String? = null,
    val data: NsiStationData? = null
)

@JsonIgnoreProperties(ignoreUnknown = true)
data class NsiStationData(
    val lat: Double? = null,
    val lon: Double? = null,
    val code: String? = null,
    val name: String? = null,
    val status: String? = null,

    @JsonProperty("elevation_m")
    val elevationM: Double? = null,

    @JsonProperty("elevation_min_deg")
    val elevationMinDeg: Double? = null
)
