package com.fabledsword.minstrel.likes.data

import com.fabledsword.minstrel.api.endpoints.LikesApi
import com.fabledsword.minstrel.auth.AuthController
import com.fabledsword.minstrel.cache.db.dao.CachedAlbumDao
import com.fabledsword.minstrel.cache.db.dao.CachedArtistDao
import com.fabledsword.minstrel.cache.db.dao.CachedLikeDao
import com.fabledsword.minstrel.cache.db.dao.CachedTrackDao
import com.fabledsword.minstrel.cache.db.entities.CachedLikeEntity
import com.fabledsword.minstrel.cache.mutations.MutationQueue
import com.fabledsword.minstrel.di.ApplicationScope
import com.fabledsword.minstrel.library.data.toDomain
import com.fabledsword.minstrel.models.AlbumRef
import com.fabledsword.minstrel.models.ArtistRef
import com.fabledsword.minstrel.models.TrackRef
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import retrofit2.Retrofit
import retrofit2.create
import javax.inject.Inject
import javax.inject.Singleton

/**
 * Cache-first reads + offline-first writes for likes. Mirrors the
 * Flutter `likesControllerProvider` + `cached_likes`-driven Liked tab
 * pattern.
 *
 * Local `userId` discriminator is the server-side user UUID
 * (resolved via [AuthController.currentUser]); falls back to
 * [ANONYMOUS_USER_ID] when no user is signed in so existing local
 * cache rows from pre-#576 builds remain queryable until the first
 * authenticated [refreshIds] call overwrites them. On user-switch
 * (sign-out / sign-in as different user) the OUTGOING user's
 * cached_likes rows are wiped so they don't leak into the new
 * session — drift #576 audit caught the leak.
 *
 * Write path follows `feedback_offline_first_for_server_writes` —
 * never fire-and-forget:
 *   1. Optimistic Room mutation (UI flips instantly).
 *   2. Best-effort server call.
 *   3. On exception → enqueue in MutationQueue for the Phase 12.2
 *      replayer to drain when connectivity returns.
 *
 * Reads expose Flows of hydrated DomainRefs. Per-tile hydration goes
 * through the existing Library DAOs — IDs without cached entity rows
 * are silently dropped (the SyncController fills the gap async).
 */
@Singleton
class LikesRepository @Inject constructor(
    private val likeDao: CachedLikeDao,
    private val albumDao: CachedAlbumDao,
    private val artistDao: CachedArtistDao,
    private val trackDao: CachedTrackDao,
    private val mutationQueue: MutationQueue,
    private val authController: AuthController,
    @ApplicationScope private val scope: CoroutineScope,
    retrofit: Retrofit,
) {
    private val api: LikesApi = retrofit.create()

    init {
        // Drift #576: clear the outgoing user's cached_likes rows
        // when the signed-in user changes. Mostly a hygiene fix —
        // discriminator-based queries already prevent the wrong
        // user from SEEING leaked rows, but without this the rows
        // pile up forever as different accounts sign in/out on the
        // same device.
        scope.launch {
            var previousId: String? = null
            authController.currentUser.collect { user ->
                val newId = user?.id
                val prev = previousId
                if (prev != null && prev != newId) {
                    likeDao.clearForUser(prev)
                }
                previousId = newId
            }
        }
    }

    /**
     * Server-user-uuid for the current session, or [ANONYMOUS_USER_ID]
     * fallback when there's no signed-in user. Used as the
     * `cached_likes.userId` discriminator so each account's likes are
     * stored under its own bucket.
     */
    private fun currentUserId(): String =
        authController.currentUser.value?.id ?: ANONYMOUS_USER_ID

    // ── Reads ─────────────────────────────────────────────────────────

    fun observeLikedArtists(): Flow<List<ArtistRef>> =
        likeDao.observeLikedIdsOfType(currentUserId(), ENTITY_ARTIST).map { ids ->
            ids.mapNotNull { artistDao.getById(it)?.toDomain() }
        }

    fun observeLikedAlbums(): Flow<List<AlbumRef>> =
        likeDao.observeLikedIdsOfType(currentUserId(), ENTITY_ALBUM).map { ids ->
            ids.mapNotNull { albumDao.getById(it)?.toDomain() }
        }

    fun observeLikedTracks(): Flow<List<TrackRef>> =
        likeDao.observeLikedIdsOfType(currentUserId(), ENTITY_TRACK).map { ids ->
            ids.mapNotNull { trackDao.getById(it)?.toDomain() }
        }

    fun observeIsLiked(entityType: String, entityId: String): Flow<Boolean> =
        likeDao.observeIsLiked(currentUserId(), entityType, entityId)

    /**
     * Reactive set of liked track ids. Use this when the caller only
     * needs "is X in liked set", NOT when it needs to render the liked
     * tracks themselves — [observeLikedTracks] does a `mapNotNull` join
     * against `trackDao` and drops any liked id whose track isn't in
     * local cache yet, so an "is liked" UI built on `observeLikedTracks`
     * misses cross-device likes whose track row hasn't cached.
     *
     * Used by playlist/album detail screens to color a row's like
     * button independent of whether the track is in the local library.
     */
    fun observeLikedTrackIds(): Flow<Set<String>> =
        likeDao.observeLikedIdsOfType(currentUserId(), ENTITY_TRACK).map { it.toSet() }

    /** One-shot snapshot of the liked track-id set — for the offline pool filter. */
    suspend fun likedTrackIds(): Set<String> =
        likeDao.observeLikedIdsOfType(currentUserId(), ENTITY_TRACK).first().toSet()

    // ── Writes (optimistic local → best-effort REST → enqueue on fail) ──

    /**
     * Toggle like state for [entityType]/[entityId]. UI doesn't need to
     * await this; the Flow re-emits immediately from the optimistic
     * Room write. Returns true if the call reached the server, false
     * if it got enqueued for later replay.
     */
    suspend fun toggleLike(entityType: String, entityId: String, desiredState: Boolean): Boolean {
        // 1. Optimistic Room mutation — scoped to the signed-in user
        //    (or the legacy "local" fallback for pre-#576 rows).
        val uid = currentUserId()
        if (desiredState) {
            likeDao.upsertAll(
                listOf(CachedLikeEntity(uid, entityType, entityId)),
            )
        } else {
            likeDao.delete(uid, entityType, entityId)
        }
        // 2. Best-effort REST call; 3. enqueue on failure.
        val kindPath = serverPathFor(entityType)
        return try {
            if (desiredState) api.like(kindPath, entityId) else api.unlike(kindPath, entityId)
            true
        } catch (
            @Suppress("TooGenericExceptionCaught", "SwallowedException") e: Throwable,
        ) {
            // Intentional swallow: the queue replays this later; surfacing
            // the network error here would force callers (UI) to handle it
            // even though it's already been buffered. The replayer logs
            // per-attempt diagnostics when it lands in Phase 12.2.
            mutationQueue.enqueueLikeToggle(entityType, entityId, desiredState)
            false
        }
    }

    /**
     * Pulls `GET /api/likes/ids` and atomically replaces the user's
     * cached_likes set with the server's canonical view. Called by
     * the LikedTab ViewModel on init (and by the SyncController in
     * Phase 12).
     *
     * Drift #570 fix: previously this called `upsertAll` only, which
     * added rows for IDs the server had but never removed rows the
     * server no longer surfaced — so a cross-device unlike (user
     * likes on web, then unlikes on web) left the Liked tab on
     * Android showing the now-unliked entry forever. The atomic
     * replaceAllForUser DAO method runs a transactional delete-then-
     * insert so the local set is exactly what the server reports.
     */
    suspend fun refreshIds() {
        val uid = currentUserId()
        val wire = api.ids()
        val rows = mutableListOf<CachedLikeEntity>()
        rows += wire.artistIds.map { CachedLikeEntity(uid, ENTITY_ARTIST, it) }
        rows += wire.albumIds.map { CachedLikeEntity(uid, ENTITY_ALBUM, it) }
        rows += wire.trackIds.map { CachedLikeEntity(uid, ENTITY_TRACK, it) }
        likeDao.replaceAllForUser(uid, rows)
    }

    companion object {
        // Pre-auth fallback discriminator. Drift #576: when no user is
        // signed in OR for cached_likes rows written by pre-#576
        // builds (which all tagged "local"), reads continue to query
        // this bucket so the Liked tab isn't suddenly empty after the
        // upgrade. The first authenticated refreshIds() call writes
        // rows under the real user UUID; reads then transparently
        // switch over via currentUserId().
        const val ANONYMOUS_USER_ID: String = "local"
        const val ENTITY_ARTIST: String = "artist"
        const val ENTITY_ALBUM: String = "album"
        const val ENTITY_TRACK: String = "track"

        private fun serverPathFor(entityType: String): String = when (entityType) {
            ENTITY_ARTIST -> "artists"
            ENTITY_ALBUM -> "albums"
            ENTITY_TRACK -> "tracks"
            else -> error("Unknown entity type: $entityType")
        }
    }
}
