/*
 * 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.PointsBoardDto
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

/**
 * The points/loyalty leaderboards (PLAN.md §9 M11, `docs/link/v3.md` §7): one board
 * per point currency the shard publishes, each with its top ranks.
 *
 * Served from the website's own store, so the page renders while the shard is down —
 * which matters more here than for live state: these are standings accumulated over
 * months, and blanking them during a restart would look like data loss.
 *
 * Kept live by `points.board` frames, one per system, merged in place by [LiveBoard].
 */
@HiltViewModel
class LeaderboardsViewModel @Inject constructor(
    private val repository: ShardRepository,
) : ViewModel() {

    private val board = LiveBoard<PointsBoardDto> { it.system.orEmpty() }

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

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

    init {
        load()
        collectLive()
    }

    fun load() {
        _state.value = UiState.Loading
        viewModelScope.launch {
            when (val result = repository.pointsBoards()) {
                is ApiResult.Ok -> {
                    board.seed(result.data)
                    publish()
                }
                else -> _state.value = result.toShardUiState()
            }
        }
    }

    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 -> applyFrame(event)
                }
            }
        }
    }

    private fun applyFrame(frame: ShardStreamEvent.Frame) {
        // There is deliberately no `points.remove` on the wire: the system set is fixed
        // for a given shard build, the same argument `city.update` makes.
        if (frame.kind != "points.board") return
        repository.pointsBoardFrame(frame.data)?.let { board.upsert(it) }
        if (_state.value is UiState.Success) publish()
    }

    private fun publish() {
        _state.value = UiState.Success(orderBoards(board.values()))
    }
}

/**
 * Board display order: most-contested first, then by name, so the boards people
 * actually compete on lead. Pure, so the ordering is unit-tested.
 *
 * Boards the shard flags as not player-facing (`showOnGump = false`) are dropped —
 * that is the shard's own "is this for players?" signal and the plugin already filters
 * on it, so this only guards a shard configured to publish extras.
 */
internal fun orderBoards(boards: Collection<PointsBoardDto>): List<PointsBoardDto> =
    boards
        .filter { it.showOnGump }
        .sortedWith(
            compareByDescending<PointsBoardDto> { it.players ?: 0 }
                .thenBy { (it.nameString ?: it.system).orEmpty().lowercase() },
        )
