package com.fabledsword.minstrel.library.ui

import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.toRoute
import com.fabledsword.minstrel.api.ErrorCopy
import com.fabledsword.minstrel.connectivity.NetworkStatusController
import com.fabledsword.minstrel.connectivity.recoveries
import com.fabledsword.minstrel.library.data.LibraryRepository
import com.fabledsword.minstrel.likes.data.LikesRepository
import com.fabledsword.minstrel.models.ArtistDetailRef
import com.fabledsword.minstrel.models.ArtistRef
import com.fabledsword.minstrel.models.TrackRef
import com.fabledsword.minstrel.nav.ArtistDetail
import com.fabledsword.minstrel.nav.DetailSeedCache
import com.fabledsword.minstrel.player.PlayerController
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject

private const val SHARE_STOP_TIMEOUT_MS = 5_000L

sealed interface ArtistDetailUiState {
    data class Loading(val seed: ArtistRef? = null) : ArtistDetailUiState
    data class Success(
        val detail: ArtistDetailRef,
        val similarArtists: List<ArtistRef> = emptyList(),
        val topTracks: List<TrackRef> = emptyList(),
    ) : ArtistDetailUiState
    data class Error(val message: String) : ArtistDetailUiState
}

@HiltViewModel
class ArtistDetailViewModel @Inject constructor(
    private val repository: LibraryRepository,
    private val likes: LikesRepository,
    private val player: PlayerController,
    private val seedCache: DetailSeedCache,
    networkStatus: NetworkStatusController,
    savedStateHandle: SavedStateHandle,
) : ViewModel() {

    private val route: ArtistDetail = savedStateHandle.toRoute()
    val artistId: String = route.id

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

    /** Transient one-shot messages for the screen-level snackbar (e.g. Play-button failure). */
    private val transientMessagesChannel = Channel<String>(Channel.BUFFERED)
    val transientMessages: Flow<String> = transientMessagesChannel.receiveAsFlow()

    val artistLiked: StateFlow<Boolean> =
        likes.observeIsLiked(LikesRepository.ENTITY_ARTIST, artistId)
            .stateIn(
                scope = viewModelScope,
                started = SharingStarted.WhileSubscribed(SHARE_STOP_TIMEOUT_MS),
                initialValue = false,
            )

    init {
        refresh()
        // Screen-level auto-recovery (issue #1245). From Error, redo the
        // whole load; from Success, re-pull just the secondary sections —
        // their fetch failures are swallowed to emptyList, so a dead zone
        // during the first load leaves them permanently absent otherwise.
        viewModelScope.launch {
            networkStatus.recoveries().collect {
                when (internal.value) {
                    is ArtistDetailUiState.Error -> refresh()
                    is ArtistDetailUiState.Success -> loadSecondarySections()
                    is ArtistDetailUiState.Loading -> Unit
                }
            }
        }
    }

    fun toggleLikeArtist() {
        val desired = !artistLiked.value
        viewModelScope.launch {
            likes.toggleLike(LikesRepository.ENTITY_ARTIST, artistId, desired)
        }
    }

    fun refresh(): Job = viewModelScope.launch {
        val carried = when (val cur = internal.value) {
            is ArtistDetailUiState.Loading -> cur.seed
            is ArtistDetailUiState.Success -> cur.detail.artist
            is ArtistDetailUiState.Error -> null
        }
        internal.value = ArtistDetailUiState.Loading(carried ?: seedCache.peekArtist(artistId))
        try {
            val detail = repository.refreshArtistDetail(artistId)
            internal.value = ArtistDetailUiState.Success(detail)
            loadSecondarySections()
        } catch (
            @Suppress("TooGenericExceptionCaught") e: Throwable,
        ) {
            internal.value = ArtistDetailUiState.Error(ErrorCopy.fromThrowable(e))
        }
    }

    /**
     * Pulls the non-critical "Similar artists" + "Top tracks" sections
     * and folds them into the live Success state. Each failure is
     * swallowed (logged only) — these sections render absent rather than
     * blocking, so the core artist view never depends on them.
     */
    private suspend fun loadSecondarySections() {
        val similar = sectionOrEmpty { repository.fetchSimilarArtists(artistId) }
        val top = sectionOrEmpty { repository.fetchArtistTopTracks(artistId) }
        val current = internal.value
        if (current is ArtistDetailUiState.Success) {
            internal.value = current.copy(similarArtists = similar, topTracks = top)
        }
    }

    private inline fun <T> sectionOrEmpty(block: () -> List<T>): List<T> =
        try {
            block()
        } catch (
            @Suppress("TooGenericExceptionCaught") e: Throwable,
        ) {
            Timber.w(e, "Artist secondary section fetch failed for %s", artistId)
            emptyList()
        }

    /**
     * Pull every track across the artist's albums, shuffle, and start
     * playing. Mirrors Flutter's artist Play button — pressing Play on
     * an artist is implicitly "shuffle this artist", not "play album by
     * album in tracklist order". Network round-trip per click; fine
     * because the user explicitly tapped Play.
     */
    fun playArtist() {
        viewModelScope.launch {
            try {
                val tracks = repository.fetchArtistTracks(artistId).shuffled()
                if (tracks.isEmpty()) {
                    transientMessagesChannel.trySend("No tracks to play for this artist.")
                } else {
                    player.setQueue(
                        tracks = tracks,
                        initialIndex = 0,
                        source = "artist:$artistId",
                    )
                }
            } catch (
                @Suppress("TooGenericExceptionCaught") e: Throwable,
            ) {
                transientMessagesChannel.trySend(
                    "Couldn't start playback: ${ErrorCopy.fromThrowable(e)}",
                )
            }
        }
    }

    /**
     * Plays the loaded "Top tracks" list starting at [index] — the top
     * tracks are themselves the playable queue (mirrors the web client's
     * sectionTracks = top-tracks behaviour). No network round-trip; the
     * list is already in the Success state.
     */
    fun playTopTracks(index: Int) {
        val tracks = (internal.value as? ArtistDetailUiState.Success)?.topTracks.orEmpty()
        if (tracks.isEmpty()) return
        player.setQueue(
            tracks = tracks,
            initialIndex = index.coerceIn(0, tracks.lastIndex),
            source = "artist-top:$artistId",
        )
    }
}
