package com.fabledsword.minstrel.home.data

import com.fabledsword.minstrel.api.endpoints.HomeApi
import com.fabledsword.minstrel.api.endpoints.MeApi
import com.fabledsword.minstrel.cache.db.dao.CachedHomeIndexDao
import com.fabledsword.minstrel.cache.db.entities.CachedHomeIndexEntity
import com.fabledsword.minstrel.metadata.HomeArtistPrewarmer
import com.fabledsword.minstrel.metadata.MetadataProvider
import com.fabledsword.minstrel.models.AlbumRef
import com.fabledsword.minstrel.models.ArtistRef
import com.fabledsword.minstrel.models.HomeTile
import com.fabledsword.minstrel.models.SystemPlaylistsStatus
import com.fabledsword.minstrel.models.TrackRef
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import retrofit2.Retrofit
import retrofit2.create
import javax.inject.Inject
import javax.inject.Singleton

/**
 * Cache-first reads for the Home screen. Per-section ID lists live in
 * `cached_home_index`; entity rows live in `cached_albums` /
 * `cached_artists` / `cached_tracks`.
 *
 * Each section is a `Flow<List<HomeTile<Ref>>>`: the index Flow drives
 * the ordered id list, and each id maps to a per-entity
 * [MetadataProvider] Flow that emits the cached row (or null while it
 * hydrates) and fires a dedup'd, concurrency-gated on-miss fetch. A
 * null [HomeTile.value] renders as a skeleton in position; the tile
 * reveals when the fetch lands and Room re-emits. Mirrors Flutter's
 * per-item tile providers.
 *
 * `refreshIndex()` pulls `GET /api/home/index`, swaps all sections in
 * one transaction (so the rows update together in a single emission
 * rather than collapsing and refilling), and pre-warms the top artists
 * via [HomeArtistPrewarmer].
 */
@Singleton
// Per-section observe accessors (one per Home row) inflate the function
// count past detekt's 11 default — accessor density, not complexity.
@Suppress("TooManyFunctions")
class HomeRepository @Inject constructor(
    private val homeIndexDao: CachedHomeIndexDao,
    private val metadataProvider: MetadataProvider,
    private val prewarmer: HomeArtistPrewarmer,
    retrofit: Retrofit,
) {
    private val api: HomeApi = retrofit.create()
    private val meApi: MeApi = retrofit.create()

    /**
     * Caller's system-playlist build state for the Home placeholder
     * cards. One-shot GET; the caller refreshes alongside the index.
     */
    suspend fun getSystemPlaylistsStatus(): SystemPlaylistsStatus =
        meApi.getSystemPlaylistsStatus().let {
            SystemPlaylistsStatus(
                inFlight = it.inFlight,
                lastRunAt = it.lastRunAt,
                lastError = it.lastError,
            )
        }

    fun observeRecentlyAddedAlbums(): Flow<List<HomeTile<AlbumRef>>> =
        observeAlbumSection(SECTION_RECENTLY_ADDED_ALBUMS)

    fun observeRediscoverAlbums(): Flow<List<HomeTile<AlbumRef>>> =
        observeAlbumSection(SECTION_REDISCOVER_ALBUMS)

    fun observeRediscoverArtists(): Flow<List<HomeTile<ArtistRef>>> =
        observeArtistSection(SECTION_REDISCOVER_ARTISTS)

    fun observeMostPlayedTracks(): Flow<List<HomeTile<TrackRef>>> =
        observeTrackSection(SECTION_MOST_PLAYED_TRACKS)

    fun observeLastPlayedArtists(): Flow<List<HomeTile<ArtistRef>>> =
        observeArtistSection(SECTION_LAST_PLAYED_ARTISTS)

    fun observeYouMightLikeAlbums(): Flow<List<HomeTile<AlbumRef>>> =
        observeAlbumSection(SECTION_YOU_MIGHT_LIKE_ALBUMS)

    fun observeYouMightLikeArtists(): Flow<List<HomeTile<ArtistRef>>> =
        observeArtistSection(SECTION_YOU_MIGHT_LIKE_ARTISTS)

    /** True when the index cache already has content on screen to protect. */
    suspend fun hasCachedIndex(): Boolean = homeIndexDao.hasAny()

    /**
     * Pulls /api/home/index and swaps every cached_home_index section in
     * a single transaction, then pre-warms the top artists. Missing
     * entity rows hydrate via the on-miss path.
     *
     * One transaction for all seven sections is deliberate: Room notifies
     * observers once, on commit, so Home swaps from the old content to
     * the new in a single emission. Per-section, un-transacted writes
     * made every row visibly collapse to empty and refill, one after
     * another (issue #2327).
     */
    suspend fun refreshIndex() {
        val wire = api.getHomeIndex()
        homeIndexDao.replaceSections(
            sections = ALL_SECTIONS,
            rows = rowsFor(SECTION_RECENTLY_ADDED_ALBUMS, "album", wire.recentlyAddedAlbums) +
                rowsFor(SECTION_REDISCOVER_ALBUMS, "album", wire.rediscoverAlbums) +
                rowsFor(SECTION_REDISCOVER_ARTISTS, "artist", wire.rediscoverArtists) +
                rowsFor(SECTION_MOST_PLAYED_TRACKS, "track", wire.mostPlayedTracks) +
                rowsFor(SECTION_LAST_PLAYED_ARTISTS, "artist", wire.lastPlayedArtists) +
                rowsFor(SECTION_YOU_MIGHT_LIKE_ALBUMS, "album", wire.youMightLikeAlbums) +
                rowsFor(SECTION_YOU_MIGHT_LIKE_ARTISTS, "artist", wire.youMightLikeArtists),
        )
        prewarmer.warm(
            wire.rediscoverArtists + wire.lastPlayedArtists + wire.youMightLikeArtists,
        )
    }

    private fun rowsFor(
        section: String,
        entityType: String,
        ids: List<String>,
    ): List<CachedHomeIndexEntity> = ids.mapIndexed { index, id ->
        CachedHomeIndexEntity(
            section = section,
            position = index,
            entityType = entityType,
            entityId = id,
        )
    }

    /**
     * The section's ordered entity ids, deduplicated.
     *
     * Room re-runs the query on every write to `cached_home_index` — and
     * `CachedHomeIndexEntity.fetchedAt` is stamped fresh each time — so
     * comparing whole rows would call every rewrite a change. Comparing
     * the id list instead means a section whose contents didn't actually
     * move never restarts the `flatMapLatest` below, which would
     * otherwise tear down and rebuild all of its tiles' hydration flows
     * and flicker unchanged tiles (issue #2327).
     */
    private fun observeSectionIds(section: String): Flow<List<String>> =
        homeIndexDao.observeBySection(section)
            .map { rows -> rows.map { it.entityId } }
            .distinctUntilChanged()

    @OptIn(ExperimentalCoroutinesApi::class)
    private fun observeAlbumSection(section: String): Flow<List<HomeTile<AlbumRef>>> =
        observeSectionIds(section).flatMapLatest { ids ->
            if (ids.isEmpty()) {
                flowOf(emptyList())
            } else {
                combine(ids.map { metadataProvider.observeAlbum(it) }) { refs ->
                    ids.mapIndexed { i, id -> HomeTile(id, refs[i]) }
                }
            }
        }

    @OptIn(ExperimentalCoroutinesApi::class)
    private fun observeArtistSection(section: String): Flow<List<HomeTile<ArtistRef>>> =
        observeSectionIds(section).flatMapLatest { ids ->
            if (ids.isEmpty()) {
                flowOf(emptyList())
            } else {
                combine(ids.map { metadataProvider.observeArtist(it) }) { refs ->
                    ids.mapIndexed { i, id -> HomeTile(id, refs[i]) }
                }
            }
        }

    @OptIn(ExperimentalCoroutinesApi::class)
    private fun observeTrackSection(section: String): Flow<List<HomeTile<TrackRef>>> =
        observeSectionIds(section).flatMapLatest { ids ->
            if (ids.isEmpty()) {
                flowOf(emptyList())
            } else {
                combine(ids.map { metadataProvider.observeTrack(it) }) { refs ->
                    ids.mapIndexed { i, id -> HomeTile(id, refs[i]) }
                }
            }
        }

    companion object {
        const val SECTION_RECENTLY_ADDED_ALBUMS = "recently_added_albums"
        const val SECTION_REDISCOVER_ALBUMS = "rediscover_albums"
        const val SECTION_REDISCOVER_ARTISTS = "rediscover_artists"
        const val SECTION_MOST_PLAYED_TRACKS = "most_played_tracks"
        const val SECTION_LAST_PLAYED_ARTISTS = "last_played_artists"
        const val SECTION_YOU_MIGHT_LIKE_ALBUMS = "you_might_like_albums"
        const val SECTION_YOU_MIGHT_LIKE_ARTISTS = "you_might_like_artists"

        /** Every section [refreshIndex] owns — the unit of one atomic swap. */
        val ALL_SECTIONS = listOf(
            SECTION_RECENTLY_ADDED_ALBUMS,
            SECTION_REDISCOVER_ALBUMS,
            SECTION_REDISCOVER_ARTISTS,
            SECTION_MOST_PLAYED_TRACKS,
            SECTION_LAST_PLAYED_ARTISTS,
            SECTION_YOU_MIGHT_LIKE_ALBUMS,
            SECTION_YOU_MIGHT_LIKE_ARTISTS,
        )
    }
}
