package com.fabledsword.minstrel.history.ui

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
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 com.fabledsword.minstrel.api.ErrorCopy
import com.fabledsword.minstrel.connectivity.NetworkStatusController
import com.fabledsword.minstrel.connectivity.recoveries
import com.fabledsword.minstrel.history.data.HistoryEntry
import com.fabledsword.minstrel.history.data.HistoryRepository
import com.fabledsword.minstrel.models.TrackRef
import com.fabledsword.minstrel.player.PlayerController
import com.fabledsword.minstrel.shared.UiState
import com.fabledsword.minstrel.shared.widgets.TrackRow
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.PullToRefreshScaffold
import com.fabledsword.minstrel.shared.widgets.TrackCoverThumb
import com.fabledsword.minstrel.shared.widgets.trackactions.TrackActionsButton
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import java.time.OffsetDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.time.format.TextStyle
import java.time.temporal.ChronoUnit
import java.util.Locale
import javax.inject.Inject

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

private const val SHARE_STOP_TIMEOUT_MS = 5_000L

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

@HiltViewModel
class HistoryTabViewModel @Inject constructor(
    private val repository: HistoryRepository,
    private val player: PlayerController,
    networkStatus: NetworkStatusController,
) : ViewModel() {

    private val refreshError = MutableStateFlow<String?>(null)

    /**
     * Cache-first: emits the cached snapshot instantly (paints from
     * disk on cold open), then SWR-refreshes underneath. Shows Error
     * only when there's nothing cached AND the refresh failed — a
     * failed refresh over a populated cache stays silent.
     */
    val uiState: StateFlow<UiState<List<HistoryEntry>>> =
        combine(repository.observeHistory(), refreshError) { page, err ->
            when {
                page != null && page.entries.isNotEmpty() -> UiState.Success(page.entries)
                page != null -> UiState.Empty
                err != null -> UiState.Error(err)
                else -> UiState.Loading
            }
        }.stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(SHARE_STOP_TIMEOUT_MS),
            initialValue = UiState.Loading,
        )

    init {
        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() }
        }
    }

    fun refresh(): Job = viewModelScope.launch {
        refreshError.value = null
        runCatching { repository.refresh() }
            .onFailure { refreshError.value = ErrorCopy.fromThrowable(it) }
    }

    /** Single-track play, matching the Flutter history-row behavior. */
    fun playTrack(track: TrackRef) {
        if (track.streamUrl.isEmpty()) return
        player.setQueue(listOf(track), initialIndex = 0, source = "history")
    }
}

// ─── Composable ──────────────────────────────────────────────────────

@Composable
fun HistoryTab(
    onNavigateToAlbum: (String) -> Unit = {},
    onNavigateToArtist: (String) -> Unit = {},
    viewModel: HistoryTabViewModel = hiltViewModel(),
    playerViewModel: com.fabledsword.minstrel.player.ui.PlayerViewModel = hiltViewModel(),
) {
    val state by viewModel.uiState.collectAsStateWithLifecycle()
    val playerState by playerViewModel.uiState.collectAsStateWithLifecycle()
    val playingTrackId = playerState.currentTrack?.id
    PullToRefreshScaffold(onRefresh = { viewModel.refresh().join() }) {
        when (val s = state) {
            UiState.Loading -> LoadingCentered()
            UiState.Empty -> EmptyState(
                title = "No listening history yet",
                body = "Play something — your recent plays will show up here.",
            )
            is UiState.Error -> ErrorRetry(
                title = "Couldn't load history",
                message = s.message,
                onRetry = { viewModel.refresh() },
            )
            is UiState.Success -> HistoryList(
                entries = s.data,
                playingTrackId = playingTrackId,
                onPlay = viewModel::playTrack,
                onNavigateToAlbum = onNavigateToAlbum,
                onNavigateToArtist = onNavigateToArtist,
            )
        }
    }
}

@Composable
private fun HistoryList(
    entries: List<HistoryEntry>,
    playingTrackId: String?,
    onPlay: (TrackRef) -> Unit,
    onNavigateToAlbum: (String) -> Unit,
    onNavigateToArtist: (String) -> Unit,
) {
    LazyColumn(modifier = Modifier.fillMaxSize()) {
        items(items = entries, key = { it.id }) { entry ->
            HistoryRow(
                entry = entry,
                nowPlaying = entry.track.id == playingTrackId,
                onClick = { onPlay(entry.track) },
                onNavigateToAlbum = onNavigateToAlbum,
                onNavigateToArtist = onNavigateToArtist,
            )
            HorizontalDivider()
        }
    }
}

@Composable
private fun HistoryRow(
    entry: HistoryEntry,
    nowPlaying: Boolean,
    onClick: () -> Unit,
    onNavigateToAlbum: (String) -> Unit,
    onNavigateToArtist: (String) -> Unit,
) {
    TrackRow(
        title = entry.track.title,
        artist = entry.track.artistName.ifEmpty { entry.track.albumTitle },
        trackId = entry.track.id,
        onClick = onClick,
        nowPlaying = nowPlaying,
        enabled = entry.track.streamUrl.isNotEmpty(),
        horizontalArrangement = Arrangement.spacedBy(12.dp),
        leading = {
            TrackCoverThumb(coverUrl = entry.track.coverUrl, contentDescription = entry.track.title)
        },
        trailing = {
            Text(
                text = relativeTime(entry.playedAt),
                style = MaterialTheme.typography.bodySmall,
                color = MaterialTheme.colorScheme.onSurfaceVariant,
            )
            TrackActionsButton(
                track = entry.track,
                onNavigateToAlbum = onNavigateToAlbum,
                onNavigateToArtist = onNavigateToArtist,
            )
        },
    )
}

// ─── Time helpers ────────────────────────────────────────────────────

private const val MINUTES_PER_HOUR = 60L
private const val HOURS_PER_DAY = 24L
private const val DAYS_PER_WEEK = 7L

/**
 * Lightweight relative-time formatter mirroring Flutter's
 * `library_screen.dart`'s `_relativeTime`:
 *
 *   < 1h   → "Nm ago"
 *   < 24h  → "Nh ago"
 *   < 7d   → "Tue 14:32"
 *   ≥ 7d   → "May 1"  (or "May 1, 2025" if different year)
 *
 * Falls back to the raw ISO string on parse failure so we never blow
 * up the row over malformed timestamps.
 */
private fun relativeTime(iso: String): String {
    val parsed = runCatching { OffsetDateTime.parse(iso) }.getOrNull()
        ?: return iso
    val local = parsed.atZoneSameInstant(ZoneId.systemDefault())
    val now = OffsetDateTime.now(ZoneId.systemDefault())
    val minutes = ChronoUnit.MINUTES.between(local, now)
    val hours = ChronoUnit.HOURS.between(local, now)
    val days = ChronoUnit.DAYS.between(local, now)
    return when {
        minutes < MINUTES_PER_HOUR -> "${minutes.coerceAtLeast(1)}m ago"
        hours < HOURS_PER_DAY -> "${hours}h ago"
        days < DAYS_PER_WEEK -> {
            val dow = local.dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.getDefault())
            val time = local.toLocalTime().format(DateTimeFormatter.ofPattern("HH:mm"))
            "$dow $time"
        }
        else -> {
            val pattern = if (local.year == now.year) "MMM d" else "MMM d, yyyy"
            local.toLocalDate().format(DateTimeFormatter.ofPattern(pattern))
        }
    }
}
