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

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.runicgateway.app.core.net.ShardStreamEvent
import com.runicgateway.app.core.result.ApiResult
import com.runicgateway.app.data.api.dto.OnlineStaffDto
import com.runicgateway.app.data.api.dto.PresenceDto
import com.runicgateway.app.data.api.dto.ShardStatusDto
import com.runicgateway.app.data.repository.ShardFeatures
import com.runicgateway.app.data.repository.ShardFeaturesRepository
import com.runicgateway.app.data.repository.ShardRepository
import com.runicgateway.app.ui.UiState
import com.runicgateway.app.ui.toShardUiState
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject

/** One row in the live activity feed. */
data class FeedLine(val id: String, val kind: String, val text: String)

/** The hub's point-in-time data (status is the primary; the rest are best-effort). */
data class ShardHub(
    val status: ShardStatusDto,
    val presence: PresenceDto?,
    val online: List<OnlineStaffDto>,
)

/**
 * The Shard hub (PLAN.md §6.2): connection status + online count + latest economy
 * + presence + online staff, over a live activity feed. Status drives the screen's
 * load state; presence/online load best-effort (a partial outage still renders what
 * it can). The SSE feed reconnects on its own (§7) and toggles the live indicator.
 */
@HiltViewModel
class ShardViewModel @Inject constructor(
    private val repository: ShardRepository,
    shardFeaturesRepository: ShardFeaturesRepository,
) : ViewModel() {

    private val _state = MutableStateFlow<UiState<ShardHub>>(UiState.Loading)
    val state: StateFlow<UiState<ShardHub>> = _state.asStateFlow()

    /**
     * Which boards to offer (M11). Read-only here — the app shell refreshes this on
     * every session change, and the hub only filters its tiles with it.
     */
    val shardFeatures: StateFlow<ShardFeatures?> = shardFeaturesRepository.features

    private val _feed = MutableStateFlow<List<FeedLine>>(emptyList())
    val feed: StateFlow<List<FeedLine>> = _feed.asStateFlow()

    private val _connected = MutableStateFlow(false)
    val connected: StateFlow<Boolean> = _connected.asStateFlow()

    private var seq = 0L

    init {
        load()
        collectLive()
    }

    fun load() {
        _state.value = UiState.Loading
        viewModelScope.launch {
            when (val status = repository.status()) {
                is ApiResult.Ok -> {
                    // Presence/online are secondary — a failure there shouldn't blank the hub.
                    val presence = (repository.presence() as? ApiResult.Ok)?.data
                    val online = (repository.online() as? ApiResult.Ok)?.data ?: emptyList()
                    _state.value = UiState.Success(ShardHub(status.data, presence, online))
                    seedFeed()
                }
                // Both error variants are ApiResult<Nothing>, so their UiState is Nothing-typed.
                is ApiResult.HttpError -> _state.value = status.toShardUiState()
                is ApiResult.NetworkError -> _state.value = status.toShardUiState()
            }
        }
    }

    private suspend fun seedFeed() {
        val events = (repository.feed(limit = 40) as? ApiResult.Ok)?.data ?: return
        _feed.value = events.map { ev ->
            FeedLine(
                id = "seed-${ev.id}",
                kind = ev.kind,
                text = ShardEventText.describe(ev.kind, ev.payload ?: kotlinx.serialization.json.JsonObject(emptyMap())),
            )
        }
    }

    private fun collectLive() {
        viewModelScope.launch {
            repository.liveEvents().collect { event ->
                when (event) {
                    is ShardStreamEvent.Open -> _connected.value = true
                    is ShardStreamEvent.Closed -> _connected.value = false
                    is ShardStreamEvent.Frame -> {
                        if (event.kind == "presence.online") updatePresence(event)
                        prepend(event)
                    }
                }
            }
        }
    }

    // Patch the live presence snapshot in place so the hub's online count tracks the
    // stream between reloads (the `presence.online` frame decodes into PresenceDto).
    private fun updatePresence(frame: ShardStreamEvent.Frame) {
        val presence = repository.presenceFrame(frame.data) ?: return
        val current = _state.value as? UiState.Success ?: return
        _state.value = UiState.Success(current.data.copy(presence = presence))
    }

    private fun prepend(frame: ShardStreamEvent.Frame) {
        val line = FeedLine(
            id = "live-${seq++}",
            kind = frame.kind,
            text = ShardEventText.describe(frame.kind, frame.data),
        )
        _feed.value = (listOf(line) + _feed.value).take(MAX_FEED)
    }

    private companion object {
        const val MAX_FEED = 40
    }
}
