package com.fabledsword.minstrel.player

import com.fabledsword.minstrel.connectivity.NetworkStatusController
import com.fabledsword.minstrel.di.ApplicationScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
import javax.inject.Singleton

private const val DEBOUNCE_MS = 2_000L

/**
 * Surfaces playback failures (load errors + zero-duration tracks) as
 * user-visible snackbar messages AND admin-inbox reports.
 *
 * Without the snackbar the player just silently skips the dead track —
 * the worst kind of bug, because the user can't tell "this file is
 * broken" from "the app is flaky". Without the admin report the
 * operator never finds out the track is bad and the next user hits
 * the same wall.
 *
 * The snackbar text mirrors Flutter's `playback_error_reporter.dart`:
 * collect [PlayerController.playbackErrorEvents], debounce in a 2s
 * window, emit "Couldn't play 'X' — skipping" for a single error or
 * "Skipped N unplayable tracks" when a burst lands inside the window.
 *
 * The server report fires per event (no debounce) via
 * [PlaybackErrorRepository], which handles the offline-first
 * MutationQueue fallback per the standing rule for server writes.
 *
 * ShellScaffold collects [messages] into its existing snackbar host
 * so the reporter doesn't need its own UI surface. Constructed at
 * app launch via the construct-the-singleton trick in
 * [com.fabledsword.minstrel.MinstrelApplication].
 */
@Singleton
class PlaybackErrorReporter @Inject constructor(
    private val playerController: PlayerController,
    private val repository: PlaybackErrorRepository,
    private val networkStatus: NetworkStatusController,
    @ApplicationScope private val scope: CoroutineScope,
) {
    private val outChannel = Channel<String>(Channel.BUFFERED)

    /** Coalesced user-facing snackbar text. Subscribers should show in a SnackbarHost. */
    val messages: Flow<String> = outChannel.receiveAsFlow()

    init {
        scope.launch {
            val buffer = mutableListOf<String>()
            var debounceJob: kotlinx.coroutines.Job? = null
            playerController.playbackErrorEvents.collect { event ->
                // A track failing to play is ambiguous (dead server vs. one bad
                // file) — let the controller arbitrate via /healthz. No-op when
                // already Offline; cheap otherwise.
                networkStatus.reportFailure()
                // Fire-and-forget the server report — repository handles
                // success/queue branching so callers don't see throws.
                scope.launch { repository.report(event) }
                buffer.add(event.title)
                debounceJob?.cancel()
                debounceJob = scope.launch {
                    delay(DEBOUNCE_MS)
                    if (buffer.isEmpty()) return@launch
                    val n = buffer.size
                    val first = buffer.first()
                    buffer.clear()
                    val msg = if (n == 1) {
                        "Couldn't play \"$first\" — skipping"
                    } else {
                        "Skipped $n unplayable tracks"
                    }
                    outChannel.trySend(msg)
                }
            }
        }
    }
}
