package com.fabledsword.minstrel.search.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.models.SearchResponseRef
import com.fabledsword.minstrel.models.TrackRef
import com.fabledsword.minstrel.player.PlayerController
import com.fabledsword.minstrel.search.data.SearchRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject

private const val DEBOUNCE_MS = 250L

sealed interface SearchResultsState {
    /** Empty query — screen shows "type to search" hint. */
    data object Idle : SearchResultsState
    data object Loading : SearchResultsState
    data class Loaded(
        val response: SearchResponseRef,
        val localOnly: Boolean = false,
    ) : SearchResultsState
    data class Error(val message: String) : SearchResultsState
}

data class SearchState(
    val query: String = "",
    val results: SearchResultsState = SearchResultsState.Idle,
)

@OptIn(FlowPreview::class)
@HiltViewModel
class SearchViewModel @Inject constructor(
    private val repository: SearchRepository,
    private val player: PlayerController,
    networkStatus: NetworkStatusController,
) : ViewModel() {

    private val queryFlow = MutableStateFlow("")
    private val internal = MutableStateFlow(SearchState())
    val state: StateFlow<SearchState> = internal.asStateFlow()

    init {
        viewModelScope.launch {
            queryFlow
                .map { it.trim() }
                .distinctUntilChanged()
                .onEach { q ->
                    // Empty query collapses results without firing the
                    // network call below.
                    if (q.isEmpty()) {
                        internal.update {
                            it.copy(results = SearchResultsState.Idle)
                        }
                    }
                }
                .debounce(DEBOUNCE_MS)
                .collect { q ->
                    if (q.isEmpty()) return@collect
                    runSearch(q)
                }
        }
        // Screen-level auto-recovery (issue #1245): a search that failed
        // while the server was unreachable re-runs when health returns —
        // the queryFlow pipeline can't, since the text hasn't changed.
        viewModelScope.launch {
            networkStatus.recoveries().collect {
                if (internal.value.results is SearchResultsState.Error) retry()
            }
        }
    }

    fun setQuery(value: String) {
        internal.update { it.copy(query = value) }
        queryFlow.value = value
    }

    fun clear() {
        setQuery("")
    }

    /**
     * Re-run the current query after a failure. Bypasses the queryFlow
     * pipeline, whose distinctUntilChanged drops a same-text resubmit —
     * without this, a transient error pins "Search failed" until the
     * user edits the query.
     */
    fun retry() {
        val q = internal.value.query.trim()
        if (q.isEmpty()) return
        viewModelScope.launch { runSearch(q) }
    }

    /**
     * Tapping a search result builds a queue from the full visible
     * track-results list starting at the tapped entry, matching Flutter.
     * Falls back to single-track playback when the loaded state isn't
     * a Success (shouldn't happen in normal flow but keeps the call
     * safe).
     */
    fun playTrack(track: TrackRef) {
        if (track.streamUrl.isEmpty()) return
        val loaded = internal.value.results as? SearchResultsState.Loaded
        val playable = loaded?.response?.tracks
            ?.filter { it.streamUrl.isNotEmpty() }
            ?.takeIf { it.isNotEmpty() }
            ?: listOf(track)
        val startIndex = playable.indexOfFirst { it.id == track.id }.coerceAtLeast(0)
        player.setQueue(playable, initialIndex = startIndex, source = "search")
    }

    private suspend fun runSearch(q: String) {
        internal.update { it.copy(results = SearchResultsState.Loading) }
        try {
            val outcome = repository.search(q)
            internal.update {
                it.copy(
                    results = SearchResultsState.Loaded(
                        response = outcome.response,
                        localOnly = outcome.localOnly,
                    ),
                )
            }
        } catch (
            @Suppress("TooGenericExceptionCaught") e: Throwable,
        ) {
            internal.update {
                it.copy(results = SearchResultsState.Error(ErrorCopy.fromThrowable(e)))
            }
        }
    }
}
