/*
 * SPDX-License-Identifier: GPL-3.0-or-later
 */
package com.runicgateway.app.core.result

import kotlinx.coroutines.CancellationException
import kotlinx.serialization.SerializationException
import retrofit2.HttpException
import java.io.IOException

/**
 * The single result type every repository call returns (PLAN.md §7). The UI
 * degrades gracefully on a down backend or shard: a repository never throws for
 * an expected failure, it returns a typed variant the screen can render.
 *
 *  - [Ok]           — a 2xx response with a decoded body.
 *  - [HttpError]    — the server answered with a non-2xx status.
 *  - [NetworkError] — the request never got an answer (offline, DNS, TLS, timeout).
 */
sealed interface ApiResult<out T> {
    data class Ok<T>(val data: T) : ApiResult<T>
    data class HttpError(val status: Int, val message: String? = null) : ApiResult<Nothing>
    data class NetworkError(val cause: Throwable) : ApiResult<Nothing>
}

/** True for the "shard/sidecar down" signal the player screens treat as offline (§6.3, §7). */
fun ApiResult<*>.isShardUnavailable(): Boolean =
    this is ApiResult.HttpError && status == 503

/** Map an [ApiResult.Ok] body while preserving the failure variants unchanged. */
inline fun <T, R> ApiResult<T>.map(transform: (T) -> R): ApiResult<R> = when (this) {
    is ApiResult.Ok -> ApiResult.Ok(transform(data))
    is ApiResult.HttpError -> this
    is ApiResult.NetworkError -> this
}

/**
 * Run a suspending Retrofit call and normalize every outcome into an [ApiResult].
 * Coroutine cancellation is rethrown so structured concurrency still works — it
 * is control flow, not a network failure.
 *
 * A body the app can't decode (a field whose type/shape doesn't match its DTO, e.g.
 * a live-shaped `guild.update` snapshot carrying an unexpected value) throws a
 * [SerializationException] out of the Retrofit converter. That is a broken contract
 * with the backend, not a bug to crash on: the request completed but the response is
 * unusable — an invalid upstream response — so it is surfaced as a server-side error
 * (`502` → [ErrorKind.SERVER]) the screen renders as "something went wrong, retry",
 * exactly the graceful-degradation the layer promises (never throw for an expected
 * failure). Without this catch the exception escapes the collecting coroutine and
 * takes down the whole app.
 */
suspend fun <T> safeApiCall(block: suspend () -> T): ApiResult<T> = try {
    ApiResult.Ok(block())
} catch (e: CancellationException) {
    throw e
} catch (e: HttpException) {
    ApiResult.HttpError(e.code(), e.message())
} catch (e: IOException) {
    ApiResult.NetworkError(e)
} catch (e: SerializationException) {
    ApiResult.HttpError(MALFORMED_RESPONSE_STATUS, e.message)
}

/** Synthetic status for a 2xx body the app couldn't decode — an invalid upstream response. */
private const val MALFORMED_RESPONSE_STATUS = 502
