@file:Suppress("MatchingDeclarationName") // VM is colocated with its Screen per project convention

package com.fabledsword.minstrel.playlists.ui

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewModelScope
import androidx.navigation.NavHostController
import com.fabledsword.minstrel.api.ErrorCopy
import com.fabledsword.minstrel.connectivity.NetworkStatusController
import com.fabledsword.minstrel.connectivity.ServerHealth
import com.fabledsword.minstrel.connectivity.recoveries
import com.fabledsword.minstrel.events.EventsStream
import com.fabledsword.minstrel.models.PlaylistRef
import com.fabledsword.minstrel.nav.PlaylistDetail
import com.fabledsword.minstrel.nav.Playlists
import com.fabledsword.minstrel.player.PlayerController
import com.fabledsword.minstrel.playlists.data.PlaylistsRepository
import com.fabledsword.minstrel.playlists.data.playPlaylistShuffled
import com.fabledsword.minstrel.shared.UiState
import com.fabledsword.minstrel.shared.asCacheFirstStateFlow
import com.fabledsword.minstrel.playlists.widgets.PlaylistCard
import com.fabledsword.minstrel.shared.widgets.ShellContentWindowInsets
import com.fabledsword.minstrel.shared.widgets.EmptyState
import com.fabledsword.minstrel.shared.widgets.ErrorRetry
import com.fabledsword.minstrel.shared.widgets.LoadingCentered
import com.fabledsword.minstrel.shared.widgets.MinstrelTopAppBar
import com.fabledsword.minstrel.shared.widgets.PullToRefreshScaffold
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.combine
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import javax.inject.Inject

private const val SHARE_STOP_TIMEOUT_MS = 5_000L

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

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

@HiltViewModel
class PlaylistsListViewModel @Inject constructor(
    private val repository: PlaylistsRepository,
    private val player: PlayerController,
    private val eventsStream: EventsStream,
    networkStatus: NetworkStatusController,
) : ViewModel() {

    private val poolMessages = Channel<String>(Channel.BUFFERED)

    /** Transient snackbar messages from playlist tile play taps. */
    val transientMessages: Flow<String> = poolMessages.receiveAsFlow()

    /** Cache-only: no link OR server unreachable (Unstable stays calm). Greys tiles. */
    val offline: StateFlow<Boolean> = networkStatus.state
        .map { it == ServerHealth.Offline || it == ServerHealth.ServerDown }
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(SHARE_STOP_TIMEOUT_MS),
            initialValue = false,
        )

    /** Copy for the most recent list-refresh failure; null once one succeeds. */
    private val refreshError = MutableStateFlow<String?>(null)

    init {
        refresh()
        // Live updates: a playlist created/updated/deleted from another
        // client should reflect here without a manual refresh.
        viewModelScope.launch {
            eventsStream.events
                .filter { it.kind.startsWith("playlist.") }
                .collect { refresh() }
        }
        // Screen-level auto-recovery (issue #1245): re-pull when server
        // health returns, so a load that failed offline heals unprompted.
        viewModelScope.launch {
            networkStatus.recoveries().collect { refresh() }
        }
    }

    /** Re-pull the playlists list. Returns the Job for pull-to-refresh awaits. */
    fun refresh(): Job = viewModelScope.launch {
        refreshError.value = null
        runCatching { repository.refreshList() }
            .onFailure { refreshError.value = ErrorCopy.fromThrowable(it) }
    }

    /** Tile play button: shuffle the playlist's tracks and start at index 0. */
    suspend fun playPlaylist(playlist: PlaylistRef) {
        viewModelScope.launch {
            playPlaylistShuffled(playlist, repository, player) {
                poolMessages.trySend(it)
            }
        }.join()
    }

    val uiState: StateFlow<UiState<List<PlaylistRef>>> =
        combine(repository.observeAll(), refreshError) { list, err ->
            when {
                list.isNotEmpty() -> UiState.Success(list)
                // Empty cache + failed refresh is a load failure, not "no
                // playlists yet" — surface Error so the user gets a Retry.
                err != null -> UiState.Error(err)
                else -> UiState.Empty
            }
        }
            .asCacheFirstStateFlow(viewModelScope)
}

// ─── Screen ──────────────────────────────────────────────────────────

@Composable
fun PlaylistsListScreen(
    navController: NavHostController,
    viewModel: PlaylistsListViewModel = hiltViewModel(),
) {
    val snackbar = remember { SnackbarHostState() }
    LaunchedEffect(Unit) {
        viewModel.transientMessages.collect { snackbar.showSnackbar(it) }
    }
    Scaffold(
        contentWindowInsets = ShellContentWindowInsets,
        modifier = Modifier.fillMaxSize(),
        topBar = {
            MinstrelTopAppBar(
                title = "Playlists",
                navController = navController,
                currentRouteName = Playlists::class.qualifiedName,
            )
        },
        snackbarHost = { SnackbarHost(snackbar) },
    ) { inner ->
        val state by viewModel.uiState.collectAsStateWithLifecycle()
        val offline by viewModel.offline.collectAsStateWithLifecycle()
        PullToRefreshScaffold(
            onRefresh = { viewModel.refresh().join() },
            modifier = Modifier.fillMaxSize().padding(inner),
        ) {
            when (val s = state) {
                UiState.Loading -> LoadingCentered()
                UiState.Empty -> EmptyState(
                    title = "No playlists yet",
                    body = "System playlists (For You / Discover / Songs like…) " +
                        "appear once your library has enough plays. Create your " +
                        "own playlist from any album or track in the meantime.",
                )
                is UiState.Error -> ErrorRetry(
                    title = "Couldn't load playlists",
                    message = s.message,
                    onRetry = { viewModel.refresh() },
                )
                is UiState.Success -> PlaylistsGrid(
                    playlists = s.data,
                    offline = offline,
                    onPlaylistClick = { id -> navController.navigate(PlaylistDetail(id)) },
                    onPlay = viewModel::playPlaylist,
                )
            }
        }
    }
}

@Composable
private fun PlaylistsGrid(
    playlists: List<PlaylistRef>,
    offline: Boolean,
    onPlaylistClick: (String) -> Unit,
    onPlay: suspend (PlaylistRef) -> Unit,
) {
    val systemPlaylists = playlists.filter { it.isSystem }
    val userPlaylists = playlists.filter { !it.isSystem }
    LazyVerticalGrid(
        columns = GridCells.Adaptive(minSize = 176.dp),
        contentPadding = PaddingValues(8.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp),
        horizontalArrangement = Arrangement.spacedBy(8.dp),
    ) {
        if (systemPlaylists.isNotEmpty()) {
            item(span = { GridItemSpan(maxLineSpan) }) {
                SectionHeader("System playlists")
            }
            items(items = systemPlaylists, key = { it.id }) { playlist ->
                // Greyed offline when it needs the live server or isn't fully
                // cached — dimmed but still tappable into the detail.
                val greyed = offline && playlist.unavailableOffline
                PlaylistCard(
                    playlist = playlist,
                    onClick = { onPlaylistClick(playlist.id) },
                    onPlay = { onPlay(playlist) },
                    playEnabled = playlist.trackCount > 0 && !greyed,
                    greyed = greyed,
                )
            }
        }
        if (userPlaylists.isNotEmpty()) {
            item(span = { GridItemSpan(maxLineSpan) }) {
                SectionHeader("Your playlists")
            }
            items(items = userPlaylists, key = { it.id }) { playlist ->
                val greyed = offline && playlist.unavailableOffline
                PlaylistCard(
                    playlist = playlist,
                    onClick = { onPlaylistClick(playlist.id) },
                    onPlay = { onPlay(playlist) },
                    playEnabled = playlist.trackCount > 0 && !greyed,
                    greyed = greyed,
                )
            }
        }
    }
}

@Composable
private fun SectionHeader(text: String) {
    Text(
        text = text,
        style = MaterialTheme.typography.titleLarge,
        color = MaterialTheme.colorScheme.onBackground,
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = 8.dp, vertical = 12.dp),
    )
}
