package space.nstart.pcp.slots_service.service

import org.springframework.beans.factory.ObjectProvider
import org.springframework.beans.factory.annotation.Value
import org.springframework.http.HttpStatus
import org.springframework.http.HttpStatusCode
import org.springframework.stereotype.Service
import org.springframework.web.reactive.function.client.ClientResponse
import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.reactive.function.client.WebClientRequestException
import org.springframework.web.server.ResponseStatusException
import org.springframework.web.util.UriComponentsBuilder
import reactor.core.publisher.Mono
import space.nstart.pcp.slots_service.dto.tgu.TguPlanDecision
import space.nstart.pcp.slots_service.dto.tgu.TguPlanDecisionMessage
import space.nstart.pcp.slots_service.dto.tgu.TguPlanResponse
import space.nstart.pcp.slots_service.dto.tgu.TguPlatformResponse
import tools.jackson.databind.ObjectMapper
import java.util.UUID

@Service
class TguPlanningService(webClientBuilderProvider: ObjectProvider<WebClient.Builder>) {
    private val webClientBuilder: WebClient.Builder = webClientBuilderProvider.ifAvailable ?: WebClient.builder()

    @Value("\${settings.tgu-service:pcp-tgu-service}")
    private lateinit var tguServiceUrl: String

    fun platforms(): List<TguPlatformResponse> =
        webClientBuilder.build()
            .get()
            .uri(buildUri("/api/platforms"))
            .retrieve()
            .onStatus({ status -> status.isError }, ::mapReadError)
            .bodyToFlux(TguPlatformResponse::class.java)
            .collectList()
            .block()
            ?: emptyList()

    fun plans(historyDays: Long): List<TguPlanResponse> =
        webClientBuilder.build()
            .get()
            .uri(buildUri("/api/plans") { builder ->
                builder.queryParam("historyDays", historyDays)
            })
            .retrieve()
            .onStatus({ status -> status.isError }, ::mapReadError)
            .bodyToFlux(TguPlanResponse::class.java)
            .collectList()
            .block()
            ?: emptyList()

    fun spacecraftPlans(spacecraftId: String, historyDays: Long): List<TguPlanResponse> =
        webClientBuilder.build()
            .get()
            .uri(buildUri("/api/plans/{spacecraftId}") { builder ->
                builder.queryParam("historyDays", historyDays)
                    .buildAndExpand(spacecraftId)
            })
            .retrieve()
            .onStatus({ status -> status.isError }, ::mapReadError)
            .bodyToFlux(TguPlanResponse::class.java)
            .collectList()
            .block()
            ?: emptyList()

    fun sendDecision(planId: UUID, decision: TguPlanDecision, reason: String?): TguPlanDecisionMessage =
        runCatching {
            webClientBuilder.build()
                .post()
                .uri(buildUri("/test/tgu/plans/{planId}/decision") { builder ->
                    builder.queryParam("decision", decision.name)
                    reason?.takeIf { it.isNotBlank() }?.let { builder.queryParam("reason", it) }
                    builder.buildAndExpand(planId)
                })
                .retrieve()
                .onStatus({ status -> status.isError }, ::mapDecisionError)
                .bodyToMono(TguPlanDecisionMessage::class.java)
                .block()
                ?: throw ResponseStatusException(
                    HttpStatus.SERVICE_UNAVAILABLE,
                    TEST_CONTROLLER_UNAVAILABLE_MESSAGE
                )
        }.getOrElse { error ->
            if (error is WebClientRequestException) {
                throw ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, TEST_CONTROLLER_UNAVAILABLE_MESSAGE, error)
            }
            throw error
        }

    /** «Снять с учёта» терминально-негативный план (pcp-tgu-service, реальный API, не тест-контроллер). */
    fun dismissPlan(planId: UUID) {
        webClientBuilder.build()
            .post()
            .uri(buildUri("/api/plans/{planId}/dismiss") { builder -> builder.buildAndExpand(planId) })
            .retrieve()
            .onStatus({ status -> status.isError }, ::mapReadError)
            .bodyToMono(Void::class.java)
            .block()
    }

    /** «Вернуть на учёт» план (pcp-tgu-service). */
    fun restorePlan(planId: UUID) {
        webClientBuilder.build()
            .post()
            .uri(buildUri("/api/plans/{planId}/restore") { builder -> builder.buildAndExpand(planId) })
            .retrieve()
            .onStatus({ status -> status.isError }, ::mapReadError)
            .bodyToMono(Void::class.java)
            .block()
    }

    fun confirmLayin(planId: UUID, reason: String?) {
        runCatching {
            webClientBuilder.build()
                .post()
                .uri(buildUri("/test/tgu/plans/{planId}/confirm-layin") { builder ->
                    reason?.takeIf { it.isNotBlank() }?.let { builder.queryParam("reason", it) }
                    builder.buildAndExpand(planId)
                })
                .retrieve()
                .onStatus({ status -> status.isError }, ::mapReadError)
                .bodyToMono(Void::class.java)
                .block()
        }.getOrElse { error ->
            if (error is WebClientRequestException) {
                throw ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, TEST_CONTROLLER_UNAVAILABLE_MESSAGE, error)
            }
            throw error
        }
    }

    private fun buildUri(
        path: String,
        customizer: (UriComponentsBuilder) -> Any = { builder -> builder.build() }
    ): java.net.URI {
        val builder = UriComponentsBuilder.fromUriString(tguServiceUrl.trimEnd('/')).path(path)
        val result = customizer(builder)
        return when (result) {
            is java.net.URI -> result
            is org.springframework.web.util.UriComponents -> result.toUri()
            else -> builder.build().toUri()
        }
    }

    private fun mapReadError(response: ClientResponse): Mono<Throwable> =
        response.bodyToMono(String::class.java)
            .defaultIfEmpty("")
            .map { body ->
                ResponseStatusException(response.statusCode(), extractErrorMessage(body, "Ошибка pcp-tgu-service"))
            }

    private fun mapDecisionError(response: ClientResponse): Mono<Throwable> =
        response.bodyToMono(String::class.java)
            .defaultIfEmpty("")
            .map { body ->
                ResponseStatusException(response.statusCode(), decisionErrorMessage(response.statusCode(), body))
            }

    private fun decisionErrorMessage(status: HttpStatusCode, body: String): String =
        when (status.value()) {
            404 -> {
                val message = extractErrorMessage(body, "")
                if (message.contains("Plan", ignoreCase = true) || message.contains("planId", ignoreCase = true)) {
                    PLAN_NOT_FOUND_MESSAGE
                } else {
                    TEST_CONTROLLER_UNAVAILABLE_MESSAGE
                }
            }
            409 -> PLAN_NOT_WAITING_MESSAGE
            in 500..599 -> TEST_CONTROLLER_UNAVAILABLE_MESSAGE
            else -> extractErrorMessage(body, "Не удалось отправить тестовое решение по плану.")
        }

    private fun extractErrorMessage(body: String, defaultMessage: String): String {
        val text = body.trim()
        if (text.isEmpty()) return defaultMessage

        return runCatching {
            val node = ObjectMapper().readTree(text)
            when {
                node.hasNonNull("reason") -> node["reason"].asText()
                node.hasNonNull("message") -> node["message"].asText()
                node.hasNonNull("error") -> node["error"].asText()
                node.hasNonNull("detail") -> node["detail"].asText()
                else -> text
            }
        }.getOrDefault(text)
    }

    private companion object {
        const val PLAN_NOT_FOUND_MESSAGE = "План не найден."
        const val PLAN_NOT_WAITING_MESSAGE =
            "План не является активным планом, ожидающим решения, или для него нет active attempt."
        const val TEST_CONTROLLER_UNAVAILABLE_MESSAGE = "Test controller недоступен или отключён."
    }
}
