package com.fabledsword.minstrel.player

import javax.inject.Inject
import javax.inject.Singleton

/**
 * Synthesized state for the UPnP route -- what the ForwardingPlayer
 * exposes via Player.getCurrentPosition / isPlaying / etc. when the
 * remote leg is active. Not a Player; a container.
 *
 * Updates flow in from:
 *  - 1Hz GetPositionInfo poll -> applyPositionInfo()
 *  - Transport SOAP calls landing 200 OK -> applyTransport{Playing,Paused,Stopped}()
 *  - Poll failures -> recordPollFailure()
 *
 * The poll-failure counter is the sole drop arbiter: [DROP_THRESHOLD]
 * consecutive poll failures = remote considered dropped (returns true
 * from recordPollFailure for the caller to surface). Any success resets it.
 * Failed transport commands no longer drop the route -- they retry and
 * otherwise defer to this counter (see MinstrelForwardingPlayer).
 */
@Singleton
class RemotePlayerState @Inject constructor() {

    @Volatile var positionMs: Long = 0L; private set
    @Volatile var durationMs: Long = 0L; private set
    @Volatile var isPlaying: Boolean = false; private set
    @Volatile var currentTrackUri: String = ""; private set
    @Volatile var lastError: Throwable? = null; private set
    @Volatile var trackNumber: Int = 0; private set

    // The operator's last explicit transport intent (play=true, pause=false),
    // distinct from the observed remote isPlaying. handleRemoteDrop resumes
    // local playback based on THIS, not isPlaying -- so a play() that fails
    // its SOAP (stale/dead renderer) still resumes on the phone instead of
    // swallowing the tap. Deliberately NOT cleared by applyError.
    @Volatile var lastPlayIntent: Boolean = false; private set

    // Pending-transport deadline (SystemClock.elapsedRealtime() at which we
    // give up waiting). When > 0, a user transport action (next/prev/seekTo
    // idx) is in flight: ForwardingPlayer has already moved the wrapped
    // player's index, but Sonos's reported Track hasn't refreshed via a
    // SOAP poll yet. PlayerController.tickPositionPoll skips track
    // adjustments while pending is non-zero. Pending clears when:
    //   (a) [event-driven, primary] a poll lands and Sonos's reported Track
    //       matches the wrapped player's currentMediaItemIndex; or
    //   (b) [safety fallback] the deadline expires (covers SOAP-fail cases
    //       where Sonos never acks).
    @Volatile var pendingTransportDeadlineMs: Long = 0L; private set

    fun beginPendingTransport(deadlineMs: Long) {
        pendingTransportDeadlineMs = deadlineMs
    }

    fun clearPendingTransport() {
        pendingTransportDeadlineMs = 0L
    }

    @Volatile private var consecutivePollFailures: Int = 0

    fun applyPositionInfo(positionMs: Long, durationMs: Long, trackUri: String, trackNumber: Int) {
        this.positionMs = positionMs
        this.durationMs = durationMs
        this.currentTrackUri = trackUri
        this.trackNumber = trackNumber
    }

    fun applyTransportPlaying() { isPlaying = true }
    fun applyTransportPaused() { isPlaying = false }
    fun applyTransportStopped() {
        isPlaying = false
        positionMs = 0L
    }

    fun setPlayIntent(intended: Boolean) { lastPlayIntent = intended }

    fun applyError(t: Throwable) {
        isPlaying = false
        lastError = t
    }

    /** Returns true when the rolling threshold trips this call. */
    fun recordPollFailure(): Boolean {
        consecutivePollFailures += 1
        return consecutivePollFailures >= DROP_THRESHOLD
    }

    fun recordPollSuccess() { consecutivePollFailures = 0 }

    fun reset() {
        positionMs = 0L
        durationMs = 0L
        isPlaying = false
        lastPlayIntent = false
        currentTrackUri = ""
        lastError = null
        consecutivePollFailures = 0
        trackNumber = 0
        pendingTransportDeadlineMs = 0L
    }

    private companion object {
        // ~30 seconds of consecutive poll failures before declaring the route
        // dropped. Bumped from 3 because screen-off WiFi sleep / brief Doze
        // can stall socket I/O for several seconds without the renderer
        // actually being unreachable -- a 3-failure drop kicked us back to
        // local audio every time the phone went into a pocket.
        const val DROP_THRESHOLD = 30
    }
}
