package com.fabledsword.minstrel.requests.ui

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
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 com.fabledsword.minstrel.models.RequestStatus
import com.fabledsword.minstrel.requests.data.CancelOutcome
import com.fabledsword.minstrel.requests.data.RequestsRepository
import com.fabledsword.minstrel.shared.UiState
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject

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

// Cadence for in-flight request polling — matches the web client (#369):
// fast enough to feel live, slow enough not to hammer the server.
private const val POLL_INTERVAL_MS = 12_000L

@HiltViewModel
class RequestsViewModel @Inject constructor(
    private val repository: RequestsRepository,
    private val eventsStream: EventsStream,
    networkStatus: NetworkStatusController,
) : ViewModel() {

    private val internal = MutableStateFlow<UiState<List<RequestRef>>>(UiState.Loading)
    val uiState: StateFlow<UiState<List<RequestRef>>> = internal.asStateFlow()

    init {
        refresh()
        // SSE push: reconciler completions arrive here; reload silently so the
        // row updates in place rather than flashing the loading spinner.
        viewModelScope.launch {
            eventsStream.events
                .filter { it.kind in RELEVANT_EVENT_KINDS }
                .collect { silentReload() }
        }
        viewModelScope.launch { pollWhileInFlight() }
        // Screen-level auto-recovery (issue #1245): a load that failed while
        // the server was unreachable heals the moment health returns, without
        // waiting for the next poll tick.
        viewModelScope.launch {
            networkStatus.recoveries().collect {
                if (internal.value is UiState.Error) silentReload()
            }
        }
    }

    fun refresh(): Job = viewModelScope.launch {
        internal.value = UiState.Loading
        try {
            val rows = repository.listMine()
            internal.value = if (rows.isEmpty()) {
                UiState.Empty
            } else {
                UiState.Success(rows)
            }
        } catch (
            @Suppress("TooGenericExceptionCaught") e: Throwable,
        ) {
            internal.value = UiState.Error(ErrorCopy.fromThrowable(e))
        }
    }

    /**
     * Auto-poll while any request is mid-ingest (status APPROVED), matching the
     * web client (#369). Reloads silently every [POLL_INTERVAL_MS] and pauses
     * when nothing is in-flight — complements the SSE push so progress stays
     * live even if the event stream drops.
     */
    private suspend fun pollWhileInFlight() {
        while (true) {
            // Also retry from a failed load (issue #1245): from UiState.Error
            // the in-flight predicate can never become true, so without this
            // the poll never self-heals the screen — e.g. after a transient
            // 500 that health monitoring (transport-level) never saw.
            if (hasInFlightRequest() || internal.value is UiState.Error) {
                silentReload()
            }
            delay(POLL_INTERVAL_MS)
        }
    }

    private fun hasInFlightRequest(): Boolean {
        val state = internal.value
        return state is UiState.Success &&
            state.data.any { it.status == RequestStatus.APPROVED }
    }

    /**
     * Reload the list in place without flipping to Loading — used by the poll
     * loop and the SSE collector so visible rows update without a spinner
     * flash. A transient failure keeps the current view; the next reload wins.
     */
    private suspend fun silentReload() {
        try {
            val rows = repository.listMine()
            internal.value = if (rows.isEmpty()) UiState.Empty else UiState.Success(rows)
        } catch (
            @Suppress("TooGenericExceptionCaught", "SwallowedException") e: Throwable,
        ) {
            // Keep the current view; the next poll / SSE / pull reconciles.
        }
    }

    /**
     * Optimistically removes [id] from the success list so the row
     * disappears immediately; on server-side success the row is
     * replaced by the canonical cancelled state from the return value.
     * On error rolls back by refetching the list.
     */
    fun cancel(id: String) {
        val before = internal.value
        if (before is UiState.Success) {
            val filtered = before.data.filterNot { it.id == id }
            internal.value = if (filtered.isEmpty()) {
                UiState.Empty
            } else {
                UiState.Success(filtered)
            }
        }
        viewModelScope.launch {
            try {
                val (outcome, updated) = repository.cancel(id)
                if (updated != null) {
                    internal.update { state ->
                        if (state !is UiState.Success) {
                            // Mid-refresh — let the refresh result win, but
                            // splice the cancelled row in so it shows the new
                            // status if the user re-pulls.
                            UiState.Success(listOf(updated))
                        } else {
                            // Re-insert the cancelled row in its old position
                            // would require remembering the prior ordering;
                            // simpler to refetch in the background.
                            state
                        }
                    }
                }
                // Drift #577: only refetch on the Synced outcome. The
                // Queued path is offline by definition — the optimistic
                // removal at the top of cancel() is correct, and a
                // refresh() call here would either (a) get the
                // not-yet-delivered cancelled row back from the server
                // and snap it into the list (confusing), or (b) fail
                // with a transport error and flip the whole screen to
                // UiState.Error, making the user think the cancel
                // failed. The mutation queue will replay the cancel
                // when connectivity returns; the next on-screen refresh
                // (pull-to-refresh, navigation back) reconciles.
                if (outcome == CancelOutcome.Synced) {
                    refresh()
                }
            } catch (
                @Suppress("TooGenericExceptionCaught", "SwallowedException") e: Throwable,
            ) {
                // Roll back by re-fetching; the failed cancel might
                // already be visible to the server (eventual consistency)
                // or not at all (transport drop). The refetch makes
                // local state match either outcome.
                refresh()
            }
        }
    }
}
