package com.fabledsword.minstrel.library.data

import com.fabledsword.minstrel.api.endpoints.LibraryApi
import com.fabledsword.minstrel.cache.db.dao.CachedAlbumDao
import com.fabledsword.minstrel.cache.db.dao.CachedArtistDao
import com.fabledsword.minstrel.cache.db.dao.CachedTrackDao
import com.fabledsword.minstrel.models.AlbumDetailRef
import com.fabledsword.minstrel.models.AlbumRef
import com.fabledsword.minstrel.models.ArtistDetailRef
import com.fabledsword.minstrel.models.ArtistRef
import com.fabledsword.minstrel.models.TrackRef
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import retrofit2.Retrofit
import retrofit2.create
import javax.inject.Inject
import javax.inject.Singleton

/**
 * Cache-first reads of artists/albums/tracks. The Room DAOs are the
 * source of truth ViewModels observe; refresh* methods pull from the
 * server and upsert into Room — the Flow emissions then propagate
 * automatically.
 *
 * Constructs its own LibraryApi from the shared Retrofit. Per the
 * "repositories own their interfaces" pattern adopted in Phase 5.1's
 * NetworkModule, we don't @Provides per-endpoint Retrofit interfaces
 * — fewer Hilt bindings, locality of reference.
 */
// LibraryRepository owns reads + refreshes for three entity families
// (artists / albums / tracks) plus shuffle. Function count scales with
// entity-family count; splitting into per-family repos would
// scatter the shared dao + retrofit plumbing for no real gain.
@Suppress("TooManyFunctions")
@Singleton
class LibraryRepository @Inject constructor(
    private val artistDao: CachedArtistDao,
    private val albumDao: CachedAlbumDao,
    private val trackDao: CachedTrackDao,
    retrofit: Retrofit,
) {
    private val api: LibraryApi = retrofit.create()

    // ---- Reads (Flow, cache-first; the cache is the source of truth) ----

    // Joins each artist to its first cached album (by sort title) so the
    // tile cover derives from /api/albums/{id}/cover — cached_artists
    // stores no cover and sync carries none. Mirrors Flutter's
    // artistTileProvider JOIN.
    fun observeArtists(): Flow<List<ArtistRef>> =
        combine(artistDao.observeAll(), albumDao.observeAll()) { artists, albums ->
            val firstAlbumByArtist = albums
                .groupBy { it.artistId }
                .mapValues { (_, list) -> list.minByOrNull { it.sortTitle }?.id ?: "" }
            artists.map { it.toDomain(coverAlbumId = firstAlbumByArtist[it.id] ?: "") }
        }

    fun observeAlbums(): Flow<List<AlbumRef>> =
        albumDao.observeAll().map { rows -> rows.map { it.toDomain() } }

    fun observeAlbumsForArtist(artistId: String): Flow<List<AlbumRef>> =
        albumDao.observeByArtist(artistId).map { rows -> rows.map { it.toDomain() } }

    fun observeTracksForAlbum(albumId: String): Flow<List<TrackRef>> =
        trackDao.observeByAlbum(albumId).map { rows -> rows.map { it.toDomain() } }

    // ---- Single-shot lookups ----

    suspend fun getArtist(id: String): ArtistRef? = artistDao.getById(id)?.toDomain()

    suspend fun getAlbum(id: String): AlbumRef? = albumDao.getById(id)?.toDomain()

    suspend fun getTrack(id: String): TrackRef? = trackDao.getById(id)?.toDomain()

    // ---- Server refreshes (sync-style writes; errors propagate) ----

    /**
     * Pulls the full artist detail (ArtistRef + albums), persists both,
     * and returns the in-memory snapshot. Lets the ArtistDetail screen
     * render from the wire response without the Room round-trip.
     */
    suspend fun refreshArtistDetail(id: String): ArtistDetailRef {
        val wire = api.getArtistDetail(id)
        artistDao.upsertAll(listOf(wire.toArtistEntity()))
        albumDao.upsertAll(wire.albums.map { it.toEntity() })
        return ArtistDetailRef(
            artist = ArtistRef(
                id = wire.id,
                name = wire.name,
                sortName = wire.sortName,
                albumCount = wire.albumCount,
                coverUrl = wire.coverUrl,
            ),
            albums = wire.albums.map { it.toDomain() },
        )
    }

    /**
     * Pulls every track for an artist (across all their albums). Used
     * by the "Play artist" button on the ArtistDetail header. No
     * persistence — the response is consumed straight into a player
     * queue.
     */
    suspend fun fetchArtistTracks(id: String): List<TrackRef> =
        api.getArtistTracks(id).map { it.toDomain() }

    /**
     * Pulls related artists for the ArtistDetail "Similar artists"
     * strip. Network-only — non-critical UI, callers swallow failures
     * and render nothing rather than blocking the screen.
     */
    suspend fun fetchSimilarArtists(id: String): List<ArtistRef> =
        api.getSimilarArtists(id).map { it.toDomain() }

    /**
     * Pulls the artist's top tracks for the ArtistDetail "Top tracks"
     * panel. Network-only; tapping a row plays this exact list as a
     * queue, so no persistence is needed.
     */
    suspend fun fetchArtistTopTracks(id: String): List<TrackRef> =
        api.getArtistTopTracks(id).map { it.toDomain() }

    /**
     * Pulls the album detail (AlbumRef + tracks), persists both, and
     * returns the in-memory snapshot. Lets the AlbumDetail screen
     * render immediately from the wire response without waiting for
     * the Room round-trip.
     */
    suspend fun refreshAlbumDetail(id: String): AlbumDetailRef {
        val wire = api.getAlbumDetail(id)
        albumDao.upsertAll(listOf(wire.toAlbumEntity()))
        trackDao.upsertAll(wire.tracks.map { it.toEntity() })
        return AlbumDetailRef(
            album = AlbumRef(
                id = wire.id,
                title = wire.title,
                artistId = wire.artistId,
                sortTitle = wire.sortTitle,
                artistName = wire.artistName,
                year = wire.year,
                trackCount = wire.trackCount,
                durationSec = wire.durationSec,
                coverUrl = wire.coverUrl,
            ),
            tracks = wire.tracks.map { it.toDomain() },
        )
    }

    /** Track detail — single track, mainly for the hydration queue. */
    suspend fun refreshTrack(id: String) {
        val wire = api.getTrack(id)
        trackDao.upsertAll(listOf(wire.toEntity()))
    }

    /**
     * "Shuffle all" — server picks random tracks across the entire
     * library. Backs the Library AppBar Shuffle button. No cache hit;
     * the user-facing semantics are "give me something fresh".
     */
    suspend fun shuffleLibrary(limit: Int = SHUFFLE_DEFAULT_LIMIT): List<TrackRef> =
        api.shuffleLibrary(limit = limit).map { it.toDomain() }

    private companion object {
        const val SHUFFLE_DEFAULT_LIMIT = 100
    }
}
