package dev.stave.cast.raw
import dev.stave.domain.renderer.PlaybackState
import kotlinx.coroutines.flow.StateFlow
import kotlin.time.Duration
/**
* Platform-agnostic surface for raw-cast observer mode — the "follow a
* Chromecast group" capability that lets the desktop UI render what some other
* sender (typically the phone) is playing on a CCA group.
*
* Desktop and Android both have concrete implementations:
* - Desktop: `dev.stave.cast.raw.RawCastObserver` (in `desktopMain`),
* delegates discovery to `CastIntegration`.
* - Android: the Hilt-injected `RawCastObserver` (in `androidMain`), uses
* `NsdManager` for discovery.
*
* This interface is what `NowPlayingViewModel` depends on, so the VM stays in
* `jvmCommonMain` and works on both targets without dragging the platform-
* specific discovery wiring up to the shared layer. Surfaces tied to a
* platform-specific device type (like the picker's `observe(device)` call) are
* NOT on the interface — those live in the platform-specific UI layer that
* already has the concrete observer in hand.
*/
interface CastObserverApi {
/** True while connected to a Cast device in observer mode. UI's `tvTrack`
* / `tvState` resolution flips on this flag. */
val observing: StateFlow<Boolean>
/** Domain [PlaybackState] mapped from the observed `MEDIA_STATUS` stream.
* Shape is identical to `Renderer.state` — UI surfaces can render either
* one without branching on which kind of source it is. */
val playbackState: StateFlow<PlaybackState>
/** Friendly name of the device being followed, or null. Picker uses this to
* show the active follow target alongside the device list. */
val connectedDeviceName: StateFlow<String?>
/** Positive evidence the followed session is alive: the receiver reports
* a media session that is not IDLE, its position isn't frozen past the
* zombie latch, and statuses haven't gone silent. Raw, NOT debounced —
* it drops to false for under a second at every track boundary, so
* consumers that paint from it must wait before believing "dead"
* (Android parity, 2026-09-03: 20 s). False when not observing. */
val sessionLive: StateFlow<Boolean>
suspend fun play()
suspend fun pause()
suspend fun stop()
suspend fun next()
suspend fun previous()
suspend fun seek(position: Duration)
suspend fun setVolume(percent: Int)
/** Repeat / shuffle of the OBSERVED session (2026-09-04, the maintainer's
* ruling: while following, these act on the group, like every other
* transport control). */
suspend fun setRepeatMode(mode: dev.stave.domain.renderer.RepeatMode)
suspend fun setShuffle(on: Boolean)
/**
* Jump the observed session to the queue row at [index] (an index into
* [playbackState].queue). Position-only — QUEUE_UPDATE with currentItemId
* never alters queue contents, so it's safe for a follower to send: the
* driving sender's index tracker follows along via its own status
* callbacks. This is the ONLY queue operation a follower may perform;
* editing (remove/reorder/insert) is deliberately prohibited because the
* sender's local queue is authoritative and would silently clobber (and
* desync from) any follower-side edit on its next reload.
*/
suspend fun jumpToQueueItem(index: Int)
/** Force a status refresh. Implementations may also do this on a timer; this
* is the manual trigger for surfaces that want fresh data right now (e.g.
* after the window regains focus). */
suspend fun requestStatusRefresh()
/** Stop observing. Idempotent. */
fun disconnect()
}