package com.fabledsword.minstrel.discover.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.discover.data.DiscoverRepository
import com.fabledsword.minstrel.discover.data.RequestOutcome
import com.fabledsword.minstrel.models.ArtistSuggestionRef
import com.fabledsword.minstrel.models.LidarrRequestKind
import com.fabledsword.minstrel.models.LidarrSearchResultRef
import com.fabledsword.minstrel.models.SuggestionSnoozeRef
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject

// ─── State ───────────────────────────────────────────────────────────

data class DiscoverState(
    val query: String = "",
    val kind: LidarrRequestKind = LidarrRequestKind.ARTIST,
    val suggestions: SuggestionState = SuggestionState.Loading,
    val results: ResultsState = ResultsState.Idle,
    val locallyRequestedMbids: Set<String> = emptySet(),
    /**
     * Parked candidates, for the manage list under the feed. Empty is the
     * normal case and hides the section entirely.
     */
    val snoozes: List<SuggestionSnoozeRef> = emptyList(),
    /**
     * Just-snoozed MBIDs. These keep their row visible showing an Undo rather
     * than yanking it out from under the user's finger; the row is gone on the
     * next load, and [snoozes] is the way back after that.
     */
    val locallySnoozedMbids: Set<String> = emptySet(),
)

sealed interface SuggestionState {
    data object Loading : SuggestionState
    data class Loaded(val items: List<ArtistSuggestionRef>) : SuggestionState
    data class Error(val message: String) : SuggestionState
}

sealed interface ResultsState {
    /** No active search — the screen shows the suggestion feed. */
    data object Idle : ResultsState
    data object Loading : ResultsState
    data class Loaded(val items: List<LidarrSearchResultRef>) : ResultsState
    data class Error(val message: String) : ResultsState
}

// ─── ViewModel ───────────────────────────────────────────────────────

@HiltViewModel
class DiscoverViewModel @Inject constructor(
    private val repository: DiscoverRepository,
    networkStatus: NetworkStatusController,
) : ViewModel() {

    private val internal = MutableStateFlow(DiscoverState())
    val state: StateFlow<DiscoverState> = internal.asStateFlow()

    init {
        loadSuggestions()
        // Screen-level auto-recovery (issue #1245): re-run whichever pane
        // failed while the server was unreachable once health returns.
        viewModelScope.launch {
            networkStatus.recoveries().collect {
                val s = internal.value
                if (s.suggestions is SuggestionState.Error) loadSuggestions()
                if (s.results is ResultsState.Error) runSearch()
            }
        }
    }

    fun setQuery(value: String) {
        internal.update {
            // Clearing the box returns to the suggestion feed.
            val newResults = if (value.isBlank()) ResultsState.Idle else it.results
            it.copy(query = value, results = newResults)
        }
    }

    fun setKind(kind: LidarrRequestKind) {
        internal.update { it.copy(kind = kind) }
        if (internal.value.query.isNotBlank()) runSearch()
    }

    fun loadSuggestions(): Job = viewModelScope.launch {
        internal.update { it.copy(suggestions = SuggestionState.Loading) }
        try {
            val items = repository.listSuggestions()
            internal.update { it.copy(suggestions = SuggestionState.Loaded(items)) }
        } catch (
            @Suppress("TooGenericExceptionCaught") e: Throwable,
        ) {
            internal.update {
                it.copy(
                    suggestions = SuggestionState.Error(
                        ErrorCopy.fromThrowable(e),
                    ),
                )
            }
        }
        // Refresh the parked list alongside the deck: a snooze made on another
        // client should show up here, and one whose window lapsed should drop
        // off. Sequenced after the deck load rather than raced with it so the
        // two panes can't disagree about a candidate mid-refresh.
        loadSnoozes()
    }

    /**
     * Loads the parked list. Failure is deliberately silent: this is a
     * secondary pane, and an error banner for it would sit above the suggestion
     * feed the user actually came for. The list stays as-is and the next
     * refresh retries.
     */
    private suspend fun loadSnoozes() {
        try {
            val rows = repository.listSnoozes()
            internal.update { it.copy(snoozes = rows) }
        } catch (
            @Suppress("TooGenericExceptionCaught", "SwallowedException") e: Throwable,
        ) {
            // Keep whatever we last showed rather than blanking the section.
        }
    }

    /**
     * Parks a suggestion. Flips the row locally first so the tap registers
     * immediately; the repository handles the offline case, so there is no
     * failure branch to revert here — unlike the web client, where the fetch
     * either lands or doesn't.
     */
    suspend fun snoozeSuggestion(s: ArtistSuggestionRef) {
        internal.update { it.copy(locallySnoozedMbids = it.locallySnoozedMbids + s.mbid) }
        repository.snoozeSuggestion(s.mbid, s.name)
        loadSnoozes()
    }

    /** Brings a parked suggestion back, from either the card or the list. */
    suspend fun unsnoozeSuggestion(mbid: String, name: String) {
        internal.update { it.copy(locallySnoozedMbids = it.locallySnoozedMbids - mbid) }
        repository.unsnoozeSuggestion(mbid, name)
        loadSnoozes()
    }

    fun runSearch() {
        val q = internal.value.query.trim()
        if (q.isEmpty()) {
            internal.update { it.copy(results = ResultsState.Idle) }
            return
        }
        viewModelScope.launch {
            internal.update { it.copy(results = ResultsState.Loading) }
            try {
                val rows = repository.search(q, internal.value.kind)
                internal.update { it.copy(results = ResultsState.Loaded(rows)) }
            } catch (
                @Suppress("TooGenericExceptionCaught") e: Throwable,
            ) {
                internal.update {
                    it.copy(results = ResultsState.Error(ErrorCopy.fromThrowable(e)))
                }
            }
        }
    }

    /**
     * Request a search-result row. Returns the outcome (ACCEPTED vs
     * QUEUED) so the screen can pick the snackbar wording and update
     * the local "requested" overlay.
     */
    suspend fun requestRow(row: LidarrSearchResultRef): RequestOutcome {
        val kind = internal.value.kind
        val outcome = repository.createRequest(
            kind = kind,
            artistMbid = if (kind == LidarrRequestKind.ARTIST) row.mbid else row.artistMbid,
            artistName = if (kind == LidarrRequestKind.ARTIST) row.name else row.secondaryText,
            albumMbid = if (kind == LidarrRequestKind.ALBUM) row.mbid else null,
            albumTitle = if (kind == LidarrRequestKind.ALBUM) row.name else null,
        )
        markRequested(row.mbid)
        return outcome
    }

    suspend fun requestSuggestion(s: ArtistSuggestionRef): RequestOutcome {
        val outcome = repository.createRequest(
            kind = LidarrRequestKind.ARTIST,
            artistMbid = s.mbid,
            artistName = s.name,
        )
        markRequested(s.mbid)
        return outcome
    }

    private fun markRequested(mbid: String) {
        internal.update { it.copy(locallyRequestedMbids = it.locallyRequestedMbids + mbid) }
    }
}
