package com.anypreta.bountyboard.data

import com.anypreta.bountyboard.data.net.ApiErrorEnvelope
import com.anypreta.bountyboard.data.net.ApiService
import com.google.gson.Gson
import retrofit2.Response

/** A failed API call carrying the server's error code + message. */
class ApiException(val code: String, override val message: String) : Exception(message)

/**
 * Thin repository over [ApiService]. Every call funnels through [call], which
 * unwraps the {"error":{code,message}} envelope into an [ApiException] on failure,
 * so ViewModels can `runCatching { repo.x() }` uniformly.
 */
class Repository(
    val api: ApiService,
    private val gson: Gson,
) {
    /** Run an API call returning a body; throws [ApiException] on non-2xx. */
    suspend fun <T> call(block: suspend ApiService.() -> Response<T>): T {
        val resp = api.block()
        if (resp.isSuccessful) {
            @Suppress("UNCHECKED_CAST")
            return (resp.body() ?: Unit) as T
        }
        throw toError(resp)
    }

    /** Run a call we only care about the success of (204s). */
    suspend fun action(block: suspend ApiService.() -> Response<Unit>) {
        val resp = api.block()
        if (!resp.isSuccessful) throw toError(resp)
    }

    private fun toError(resp: Response<*>): ApiException {
        val raw = try {
            resp.errorBody()?.string()
        } catch (_: Exception) {
            null
        }
        if (!raw.isNullOrBlank()) {
            try {
                val env = gson.fromJson(raw, ApiErrorEnvelope::class.java)
                env?.error?.let {
                    return ApiException(it.code.ifEmpty { "error" }, it.message.ifEmpty { friendly(resp.code()) })
                }
            } catch (_: Exception) {
                // fall through
            }
        }
        return ApiException("http_${resp.code()}", friendly(resp.code()))
    }

    private fun friendly(code: Int): String = when (code) {
        401 -> "Your session expired. Please sign in again."
        403 -> "You don't have permission to do that."
        404 -> "Not found."
        409 -> "This changed since you loaded it — reload and try again."
        429 -> "Too many attempts. Please wait a moment."
        in 500..599 -> "The server had a problem. Try again shortly."
        else -> "Request failed ($code)."
    }
}
