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

import com.runicgateway.app.core.result.ApiResult

/**
 * The load state a screen renders. View models expose a `StateFlow<UiState<T>>`;
 * the Compose layer renders loading / content / error+retry from it (PLAN.md §7:
 * clean loading/error/retry states, no crash on a down backend).
 */
sealed interface UiState<out T> {
    data object Loading : UiState<Nothing>
    data class Success<T>(val data: T) : UiState<T>
    data class Error(val kind: ErrorKind, val httpStatus: Int? = null) : UiState<Nothing>
}

/**
 * Coarse failure buckets the UI turns into a friendly message + retry. Kept
 * transport-agnostic so a screen picks the right copy without inspecting raw
 * exceptions or status codes.
 */
enum class ErrorKind {
    /** No answer from the backend — offline, DNS, TLS, timeout. */
    NETWORK,

    /** Requested item doesn't exist (404). */
    NOT_FOUND,

    /** Too many requests (429) — back off and retry shortly. */
    RATE_LIMITED,

    /** Shard/sidecar down (503) — shard reads only; render as offline (§6.3). */
    SHARD_OFFLINE,

    /**
     * This shard doesn't publish the surface, or doesn't publish it to this viewer
     * (M11). Distinct from [NOT_FOUND] and [SHARD_OFFLINE]: the site is up, the shard
     * may well be up, and retrying changes nothing — an admin decides this.
     */
    FEATURE_UNAVAILABLE,

    /** Any other non-2xx server response. */
    SERVER,
}

/** Fold an [ApiResult] into a [UiState], mapping known statuses to [ErrorKind]s. */
fun <T> ApiResult<T>.toUiState(): UiState<T> = when (this) {
    is ApiResult.Ok -> UiState.Success(data)
    is ApiResult.NetworkError -> UiState.Error(ErrorKind.NETWORK)
    is ApiResult.HttpError -> UiState.Error(
        kind = when (status) {
            404 -> ErrorKind.NOT_FOUND
            429 -> ErrorKind.RATE_LIMITED
            503 -> ErrorKind.SHARD_OFFLINE
            else -> ErrorKind.SERVER
        },
        httpStatus = status,
    )
}

/**
 * [toUiState] for a **shard-derived** read, where `404` carries a second meaning.
 *
 * The website's `requireFeature` gate answers `404` when a feature is switched off —
 * deliberately, so the response doesn't disclose that the surface exists — and `403`
 * when it's on but the caller is below its audience rung (`docs/link/v3.md` §3.6).
 * On these routes a `404` therefore almost never means "no such thing"; it means this
 * shard doesn't publish it. Rendering "couldn't be found" with a retry button would
 * invite the user to retry something an admin controls.
 *
 * Kept as a separate mapper rather than folded into [toUiState] because both statuses
 * mean something else off the shard surface: `404` is a genuinely missing item (a
 * deleted post, an unknown wiki slug) and `403` is an ownership or role refusal on a
 * player or admin route, which is not an admin's visibility setting.
 */
fun <T> ApiResult<T>.toShardUiState(): UiState<T> = when (this) {
    is ApiResult.HttpError -> if (status == 403 || status == 404) {
        UiState.Error(ErrorKind.FEATURE_UNAVAILABLE, httpStatus = status)
    } else {
        toUiState()
    }
    else -> toUiState()
}
