package com.fabledsword.minstrel.admin.ui

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.fabledsword.minstrel.admin.data.AdminRequestsRepository
import com.fabledsword.minstrel.admin.data.AdminUsersRepository
import com.fabledsword.minstrel.api.ErrorCopy
import com.fabledsword.minstrel.connectivity.NetworkStatusController
import com.fabledsword.minstrel.connectivity.recoveries
import com.fabledsword.minstrel.events.EventsStream
import com.fabledsword.minstrel.models.RequestRef
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch
import javax.inject.Inject

private val RELEVANT_EVENT_KINDS = setOf("request.status_changed")

sealed interface AdminRequestsUiState {
    data object Loading : AdminRequestsUiState
    data object Empty : AdminRequestsUiState
    data class Success(
        val rows: List<RequestRef>,
        val usernames: Map<String, String>,
    ) : AdminRequestsUiState
    data class Error(val message: String) : AdminRequestsUiState
}

@HiltViewModel
class AdminRequestsViewModel @Inject constructor(
    private val repository: AdminRequestsRepository,
    private val usersRepository: AdminUsersRepository,
    private val eventsStream: EventsStream,
    networkStatus: NetworkStatusController,
) : ViewModel() {

    private val internal = MutableStateFlow<AdminRequestsUiState>(AdminRequestsUiState.Loading)
    val uiState: StateFlow<AdminRequestsUiState> = internal.asStateFlow()

    init {
        refresh()
        viewModelScope.launch {
            eventsStream.events
                .filter { it.kind in RELEVANT_EVENT_KINDS }
                .collect { refresh() }
        }
        // Screen-level auto-recovery (issue #1245): reload a failed list
        // when server health returns instead of waiting for a manual pull.
        viewModelScope.launch {
            networkStatus.recoveries().collect {
                if (internal.value is AdminRequestsUiState.Error) refresh()
            }
        }
    }

    fun refresh(): Job = viewModelScope.launch {
        internal.value = AdminRequestsUiState.Loading
        try {
            val (rows, usernames) = coroutineScope {
                val requestsJob = async { repository.list() }
                val usernamesJob = async {
                    runCatching {
                        usersRepository.list().associate { it.id to it.username }
                    }.getOrDefault(emptyMap())
                }
                requestsJob.await() to usernamesJob.await()
            }
            internal.value = if (rows.isEmpty()) {
                AdminRequestsUiState.Empty
            } else {
                AdminRequestsUiState.Success(rows = rows, usernames = usernames)
            }
        } catch (
            @Suppress("TooGenericExceptionCaught") e: Throwable,
        ) {
            internal.value = AdminRequestsUiState.Error(
                ErrorCopy.fromThrowable(e),
            )
        }
    }

    fun approve(id: String) = decide(id) { repository.approve(it) }

    fun reject(id: String) = decide(id) { repository.reject(it) }

    /**
     * Optimistically removes the decided row from the visible list and
     * fires the REST call. On failure, refetches so the UI matches the
     * server's view rather than leaving a phantom hole.
     */
    private fun decide(id: String, action: suspend (String) -> Unit) {
        val before = internal.value
        if (before is AdminRequestsUiState.Success) {
            val filtered = before.rows.filterNot { it.id == id }
            internal.value = if (filtered.isEmpty()) {
                AdminRequestsUiState.Empty
            } else {
                AdminRequestsUiState.Success(rows = filtered, usernames = before.usernames)
            }
        }
        viewModelScope.launch {
            try {
                action(id)
            } catch (
                @Suppress("TooGenericExceptionCaught", "SwallowedException") e: Throwable,
            ) {
                // Refetch reconciles whatever the server now says about
                // this request; the error is intentionally swallowed
                // because the refresh result IS the user-facing signal.
                refresh()
            }
        }
    }
}
