package com.fabledsword.minstrel.quarantine.data

import com.fabledsword.minstrel.api.endpoints.FlagRequest
import com.fabledsword.minstrel.api.endpoints.QuarantineApi
import com.fabledsword.minstrel.cache.db.dao.CachedQuarantineDao
import com.fabledsword.minstrel.cache.db.entities.CachedQuarantineEntity
import com.fabledsword.minstrel.cache.mutations.MutationQueue
import com.fabledsword.minstrel.models.QuarantineRef
import com.fabledsword.minstrel.models.TrackRef
import com.fabledsword.minstrel.models.wire.QuarantineMineWire
import com.fabledsword.minstrel.shared.secondsToMillis
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.datetime.Clock
import retrofit2.Retrofit
import retrofit2.create
import javax.inject.Inject
import javax.inject.Singleton

/**
 * Outcome of an `unflag` call — mirrors LikesRepository.toggleLike's
 * RequestOutcome flavor. ACCEPTED = server confirmed; QUEUED = the
 * call landed in the MutationQueue for later replay.
 */
enum class UnflagOutcome { ACCEPTED, QUEUED }

/**
 * Outcome of a `flag` call. Same semantics as [UnflagOutcome] —
 * ACCEPTED = server confirmed; QUEUED = the call landed in the
 * MutationQueue for later replay.
 */
enum class FlagOutcome { ACCEPTED, QUEUED }

/**
 * Cache-first accessor for the caller's quarantine list + offline-
 * first flag / unflag writes. Mirrors Flutter's `MyQuarantineController`
 * (drift `cached_quarantine_mine` + SWR + optimistic mutations) using
 * the native Room + Flow idiom:
 *
 *   - [observeMine] emits the cached rows, so the Hidden tab paints
 *     from disk instantly and survives connectivity loss.
 *   - [refresh] full-replaces the table from the server snapshot in a
 *     single transaction (no delete-then-insert flicker).
 *   - [flag] / [unflag] mutate the cache optimistically (the Flow
 *     re-emits immediately), call the server, and enqueue on failure
 *     so the intent persists offline. The optimistic row is NOT rolled
 *     back on failure — same as Flutter.
 */
@Singleton
class QuarantineRepository @Inject constructor(
    private val dao: CachedQuarantineDao,
    private val mutationQueue: MutationQueue,
    retrofit: Retrofit,
) {
    private val api: QuarantineApi = retrofit.create()

    /** Cache-first stream of the caller's hidden tracks (newest first). */
    fun observeMine(): Flow<List<QuarantineRef>> =
        dao.observeAll().map { rows -> rows.map { it.toDomain() } }

    /** Pull the server snapshot and atomically replace the cached table. */
    suspend fun refresh() {
        val fresh = api.listMine().map { it.toEntity() }
        dao.replaceAll(fresh)
    }

    /** Server snapshot without touching the cache — used by the TrackActions hidden check. */
    suspend fun listMine(): List<QuarantineRef> = api.listMine().map { it.toDomain() }

    /**
     * Flag [track] for quarantine with [reason] (bad_rip / wrong_file /
     * wrong_tags / duplicate / other) and optional admin-visible
     * [notes]. Optimistically inserts the cache row (the Hidden tab +
     * isHidden checks update instantly), then calls the server;
     * transport failure enqueues for replay (server re-flag is
     * idempotent).
     */
    suspend fun flag(track: TrackRef, reason: String, notes: String): FlagOutcome {
        dao.upsert(track.toQuarantineEntity(reason, notes))
        return try {
            api.flag(FlagRequest(trackId = track.id, reason = reason, notes = notes))
            FlagOutcome.ACCEPTED
        } catch (
            @Suppress("TooGenericExceptionCaught", "SwallowedException") e: Throwable,
        ) {
            mutationQueue.enqueueQuarantineFlag(track.id, reason, notes)
            FlagOutcome.QUEUED
        }
    }

    suspend fun unflag(trackId: String): UnflagOutcome {
        dao.deleteByTrackId(trackId)
        return try {
            api.unflag(trackId)
            UnflagOutcome.ACCEPTED
        } catch (
            @Suppress("TooGenericExceptionCaught", "SwallowedException") e: Throwable,
        ) {
            mutationQueue.enqueueQuarantineUnflag(trackId)
            UnflagOutcome.QUEUED
        }
    }
}

// ── Mappers (internal — wire/entity stay out of UI) ──

private fun QuarantineMineWire.toEntity(): CachedQuarantineEntity = CachedQuarantineEntity(
    trackId = trackId,
    reason = reason,
    notes = notes,
    createdAt = createdAt,
    trackTitle = trackTitle,
    trackDurationMs = trackDurationMs,
    albumId = albumId,
    albumTitle = albumTitle,
    albumCoverArtPath = albumCoverArtPath,
    artistId = artistId,
    artistName = artistName,
)

private fun CachedQuarantineEntity.toDomain(): QuarantineRef = QuarantineRef(
    trackId = trackId,
    reason = reason,
    notes = notes,
    createdAt = createdAt,
    trackTitle = trackTitle,
    trackDurationMs = trackDurationMs,
    albumId = albumId,
    albumTitle = albumTitle,
    albumCoverArtPath = albumCoverArtPath,
    artistId = artistId,
    artistName = artistName,
)

private fun QuarantineMineWire.toDomain(): QuarantineRef = toEntity().toDomain()

private fun TrackRef.toQuarantineEntity(reason: String, notes: String): CachedQuarantineEntity =
    CachedQuarantineEntity(
        trackId = id,
        reason = reason,
        notes = notes.ifEmpty { null },
        createdAt = Clock.System.now().toString(),
        trackTitle = title,
        trackDurationMs = durationSec.secondsToMillis(),
        albumId = albumId,
        albumTitle = albumTitle,
        albumCoverArtPath = null,
        artistId = artistId,
        artistName = artistName,
    )
