/*
 * SPDX-License-Identifier: GPL-3.0-or-later
 */
package com.runicgateway.app.data.repository

import com.runicgateway.app.core.net.ShardStream
import com.runicgateway.app.core.net.ShardStreamEvent
import com.runicgateway.app.core.result.ApiResult
import com.runicgateway.app.core.result.safeApiCall
import com.runicgateway.app.data.api.PublicApi
import com.runicgateway.app.data.api.dto.AtlasCreatureDto
import com.runicgateway.app.data.api.dto.AtlasCreaturePageDto
import com.runicgateway.app.data.api.dto.ChampDto
import com.runicgateway.app.data.api.dto.EconomySampleDto
import com.runicgateway.app.data.api.dto.FeedEventDto
import com.runicgateway.app.data.api.dto.GovernorDto
import com.runicgateway.app.data.api.dto.GovernorTermDto
import com.runicgateway.app.data.api.dto.GuildDto
import com.runicgateway.app.data.api.dto.HouseDto
import com.runicgateway.app.data.api.dto.MarketMetaDto
import com.runicgateway.app.data.api.dto.MarketPageDto
import com.runicgateway.app.data.api.dto.MarketVendorDto
import com.runicgateway.app.data.api.dto.OnlineStaffDto
import com.runicgateway.app.data.api.dto.PointsBoardDto
import com.runicgateway.app.data.api.dto.PresenceDto
import com.runicgateway.app.data.api.dto.RulesetDto
import com.runicgateway.app.data.api.dto.ShardStatusDto
import kotlinx.coroutines.flow.Flow
import kotlinx.serialization.KSerializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import javax.inject.Inject
import javax.inject.Singleton

/**
 * The public shard widgets (PLAN.md §6.2): point-in-time board snapshots over
 * the `/public/shard/…` GETs plus the live SSE stream. Every read returns a typed
 * [ApiResult] so a down shard renders as offline (§7); [liveEvents] is the shared
 * SSE feed the boards merge in place. Live `*.update` frames decode into the same
 * DTOs as the snapshot reads via the `*Frame` decoders.
 */
@Singleton
class ShardRepository @Inject constructor(
    private val api: PublicApi,
    private val stream: ShardStream,
    private val json: Json,
) {
    // ── Snapshots ────────────────────────────────────────────────────────
    suspend fun status(): ApiResult<ShardStatusDto> = safeApiCall { api.getShardStatus() }

    suspend fun feed(limit: Int = 40): ApiResult<List<FeedEventDto>> =
        safeApiCall { api.getShardFeed(limit = limit) }

    suspend fun economy(limit: Int = 100): ApiResult<List<EconomySampleDto>> =
        safeApiCall { api.getShardEconomy(limit) }

    suspend fun online(): ApiResult<List<OnlineStaffDto>> = safeApiCall { api.getShardOnline() }

    suspend fun presence(): ApiResult<PresenceDto> = safeApiCall { api.getShardPresence() }

    suspend fun champs(): ApiResult<List<ChampDto>> = safeApiCall { api.getShardChamps() }

    suspend fun guilds(): ApiResult<List<GuildDto>> = safeApiCall { api.getShardGuilds() }

    suspend fun governors(): ApiResult<List<GovernorDto>> = safeApiCall { api.getShardGovernors() }

    suspend fun governorHistory(city: String, limit: Int = 25): ApiResult<List<GovernorTermDto>> =
        safeApiCall { api.getShardGovernorHistory(city, limit) }

    suspend fun houses(): ApiResult<List<HouseDto>> = safeApiCall { api.getShardHouses() }

    // ── Protocol 3.0 shard content (§9 M11) ──────────────────────────────
    //
    // All four sit behind `requireFeature`, so a 404/403 here is "this shard doesn't
    // publish it" rather than a fault — see `toShardUiState()`.

    /** The shard ruleset, or `Ok(null)` when the shard has never published one. */
    suspend fun ruleset(): ApiResult<RulesetDto?> = safeApiCall { api.getShardRuleset() }

    suspend fun pointsBoards(): ApiResult<List<PointsBoardDto>> = safeApiCall { api.getShardPoints() }

    suspend fun pointsBoard(system: String): ApiResult<PointsBoardDto> =
        safeApiCall { api.getShardPointsBoard(system) }

    suspend fun market(
        query: String? = null,
        map: String? = null,
        region: String? = null,
        sort: String = SORT_PRICE_ASC,
        limit: Int = MARKET_PAGE,
        offset: Int = 0,
    ): ApiResult<MarketPageDto> = safeApiCall {
        api.getShardMarket(
            query = query?.takeIf { it.isNotBlank() },
            map = map?.takeIf { it.isNotBlank() },
            region = region?.takeIf { it.isNotBlank() },
            sort = sort,
            limit = limit,
            offset = offset,
        )
    }

    suspend fun marketMeta(): ApiResult<MarketMetaDto> = safeApiCall { api.getShardMarketMeta() }

    suspend fun marketVendor(serial: String): ApiResult<MarketVendorDto> =
        safeApiCall { api.getShardMarketVendor(serial) }

    suspend fun atlasCreatures(
        query: String? = null,
        facet: String? = null,
        limit: Int = ATLAS_PAGE,
        offset: Int = 0,
    ): ApiResult<AtlasCreaturePageDto> = safeApiCall {
        api.getAtlasCreatures(
            query = query?.takeIf { it.isNotBlank() },
            facet = facet?.takeIf { it.isNotBlank() },
            limit = limit,
            offset = offset,
        )
    }

    suspend fun atlasCreature(slug: String): ApiResult<AtlasCreatureDto> =
        safeApiCall { api.getAtlasCreature(slug) }

    // ── Live stream ──────────────────────────────────────────────────────
    /** The shared public SSE feed (safe kinds only), reconnecting with backoff (§7). */
    fun liveEvents(): Flow<ShardStreamEvent> = stream.events()

    // Decode a live `*.update` frame into the board DTO it mirrors; null on shape
    // mismatch so a malformed frame is skipped rather than crashing the board.
    fun champFrame(obj: JsonObject): ChampDto? = decode(obj, ChampDto.serializer())
    fun guildFrame(obj: JsonObject): GuildDto? = decode(obj, GuildDto.serializer())
    fun governorFrame(obj: JsonObject): GovernorDto? = decode(obj, GovernorDto.serializer())
    fun presenceFrame(obj: JsonObject): PresenceDto? = decode(obj, PresenceDto.serializer())

    // Protocol 3.0 frames. `world.ruleset` and `points.board` ride the public stream by
    // default; `vendor.listing` does NOT — the market feature ships with its SSE fan-out
    // disabled (a live firehose of vendor inventories would be the site's biggest
    // bandwidth consumer), so the market screen is a plain paginated read and must never
    // wait on a frame.
    fun rulesetFrame(obj: JsonObject): RulesetDto? = decode(obj, RulesetDto.serializer())
    fun pointsBoardFrame(obj: JsonObject): PointsBoardDto? = decode(obj, PointsBoardDto.serializer())

    private fun <T> decode(obj: JsonObject, serializer: KSerializer<T>): T? = try {
        json.decodeFromJsonElement(serializer, obj)
    } catch (_: Exception) {
        null
    }

    companion object {
        const val SORT_PRICE_ASC = "price_asc"
        const val SORT_PRICE_DESC = "price_desc"
        const val SORT_RECENT = "recent"

        /** The server caps `limit` at 100; stay well under it on a phone. */
        const val MARKET_PAGE = 50
        const val ATLAS_PAGE = 50
    }
}
