package com.fabledsword.minstrel.player

import com.fabledsword.minstrel.cache.db.dao.CachedResumeStateDao
import com.fabledsword.minstrel.cache.db.entities.CachedResumeStateEntity
import com.fabledsword.minstrel.di.ApplicationScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton

/**
 * Persists the player's last queue + position to Room so a torn-down
 * session can resume on next app launch. Mirrors
 * `flutter_client/lib/cache/resume_controller.dart`.
 *
 * Subscribes to [PlayerController.uiState] in init; persists when the
 * (track-id, queueIndex) changes — captures real session transitions
 * (track started / skipped) without churning on every 1Hz position tick.
 *
 * Restore is one-shot at app start (called from `MinstrelApplication.
 * onCreate`). Reads the row, decodes, calls
 * [PlayerController.setQueue] — the player's transport buttons + any
 * paired Wear/lock-screen surface now have something to act on even
 * if the user never opened the in-app UI this run.
 */
@Singleton
class ResumeController @Inject constructor(
    private val playerController: PlayerController,
    private val dao: CachedResumeStateDao,
    private val json: Json,
    @ApplicationScope private val scope: CoroutineScope,
) {
    init {
        scope.launch { observeAndPersist() }
    }

    suspend fun restore() {
        // runCatching folds row-missing + decode-failure + empty-tracks into
        // a single null-or-go-ahead expression so detekt's ReturnCount rule
        // is satisfied. The .onFailure side effect drops a corrupted row
        // rather than leaving a poison pill across launches.
        val row = dao.get() ?: return
        val payload =
            runCatching { json.decodeFromString<ResumePayload>(row.json) }
                .onFailure {
                    Timber.w(it, "ResumeController: dropping unreadable resume state")
                    dao.clear()
                }
                .getOrNull()
                ?.takeIf { it.tracks.isNotEmpty() }
                ?: return
        // Drift #562: wait for the MediaController binding before
        // calling setQueue, otherwise restore() racing the IPC
        // handshake silently drops the persisted queue (PlayerController
        // setQueue early-returns when mediaController is null).
        playerController.awaitReady()
        playerController.setQueue(
            tracks = payload.tracks,
            initialIndex = payload.queueIndex
                .coerceAtLeast(0)
                .coerceAtMost(payload.tracks.lastIndex),
            source = payload.source,
            // Drift #560: cold-boot resume must NOT auto-start
            // playback. The user has been away from the app; starting
            // audio on cold launch is surprising. They tap play to
            // resume. Queue + position are restored; transport is idle.
            autoplay = false,
        )
        // PositionMs not seeked yet — Player connection may still be
        // establishing; the seek call would no-op. Acceptable to start
        // from 0 on resume for now; #future-polish if it bothers anyone.
    }

    private suspend fun observeAndPersist() {
        playerController.uiState
            // Skip the initial "empty" snapshot before anything has been played.
            .drop(1)
            .map { Triple(it.currentTrack?.id, it.queueIndex, it.queue.size) }
            .distinctUntilChanged()
            .collect { persistCurrentSnapshot() }
    }

    private suspend fun persistCurrentSnapshot() {
        val state = playerController.uiState.value
        if (state.queue.isEmpty()) {
            // Clear so an emptied queue doesn't auto-restore stale tracks.
            dao.clear()
            return
        }
        val payload = ResumePayload(
            tracks = state.queue,
            queueIndex = state.queueIndex.coerceAtLeast(0),
            positionMs = state.positionMs,
        )
        dao.upsert(
            CachedResumeStateEntity(
                json = json.encodeToString(ResumePayload.serializer(), payload),
            ),
        )
    }
}
