package com.ponzischeme89.memby.data

import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.booleanOrNull
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.intOrNull
import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonArray

/**
 * The settings that belong to a *person* rather than to a television, in the shape both
 * ends of the sync agree on.
 *
 * What is here and what is not is the whole design. These follow a viewer to any set they
 * sign into and are the ones an operator can push. Deliberately absent: the device name,
 * the update source and token, the screensaver's rotation interval and ring colour, and
 * the last backdrop — those describe the box in the living room, and carrying them across
 * would rename someone's other television the moment they signed in on it.
 *
 * The class is a plain data holder with no Android or DataStore dependency so the
 * conversions either side of it stay unit-testable.
 */
data class UserPreferences(
    val homeSections: List<String> = DEFAULT_SECTIONS,
    val homeCardDensity: String = Settings.DEFAULT_HOME_CARD_DENSITY,
    val homeArtworkStyle: String = Settings.DEFAULT_HOME_ARTWORK_STYLE,
    val showHomeCardMetadata: Boolean = true,
    val showRatingsStrip: Boolean = true,
    val hideWatchedMovies: Boolean = false,
    val showTitleLogo: Boolean = true,
    val welcomeQuoteStyle: String = Settings.DEFAULT_WELCOME_QUOTE_STYLE,
    /**
     * The colour scheme this viewer chose, as a server theme id.
     *
     * Deliberately not validated on this side, unlike [seekIntervalSeconds] and
     * [skipIntroMode]. Those normalise because a value this build cannot read would reach
     * the player as a behaviour — an unknown skip length, an unexplained jump. A theme id is
     * only ever handed back to the gateway, which is the thing that decides what it means,
     * and the palette that arrives is never derived from it here. So the safe treatment is
     * to carry an unrecognised id through untouched, which is also what lets the server grow
     * a theme before every television in the house has any idea it exists.
     */
    val themeId: String = Settings.DEFAULT_THEME_ID,
    val autoPlayNextEpisode: Boolean = true,
    val showTenMinuteReminder: Boolean = true,
    /**
     * Whether a subtitle track is turned on automatically, and which language wins when it
     * is. These sync for the same reason the rest do — turning subtitles off in the bedroom
     * should not leave them on in the living room — and the gateway is what applies them,
     * since it is the thing that asks Emby which tracks exist.
     */
    val subtitlesEnabled: Boolean = true,
    val subtitleLanguage: String = SUBTITLE_LANGUAGE_AUTO,
    /** How far Left and Right move the film, in seconds. One of [SEEK_INTERVAL_SECONDS]. */
    val seekIntervalSeconds: Int = DEFAULT_SEEK_INTERVAL_SECONDS,
    /** What happens at an episode's opening titles. One of [SKIP_INTRO_MODES]. */
    val skipIntroMode: String = DEFAULT_SKIP_INTRO_MODE,
    /** Shrink the closing credits to one side at double speed with what is on next beside. */
    val speedUpCredits: Boolean = true,
    val forYouMinutes: Int = 0,
    val homeRowOrder: List<String> = emptyList(),
    val homePinnedRows: List<String> = emptyList(),
    val homeHiddenRows: List<String> = emptyList(),
) {
    companion object {
        val DEFAULT_SECTIONS: List<String> = Settings.DEFAULT_HOME_SECTIONS.split(",")
    }
}

/**
 * What this television currently believes, ready to be pushed up.
 *
 * Reads the flat active-profile keys rather than the profile list: those are the values
 * every screen actually renders from, so this is the state a viewer would recognise.
 */
fun Settings.toUserPreferences(): UserPreferences = UserPreferences(
    homeSections = homeSections.decodeCommaList(),
    homeCardDensity = homeCardDensity,
    homeArtworkStyle = homeArtworkStyle,
    showHomeCardMetadata = showHomeCardMetadata,
    showRatingsStrip = showRatingsStrip,
    hideWatchedMovies = hideWatchedMovies,
    showTitleLogo = showTitleLogo,
    welcomeQuoteStyle = welcomeQuoteStyle,
    themeId = themeId,
    autoPlayNextEpisode = autoPlayNextEpisode,
    showTenMinuteReminder = showTenMinuteReminder,
    subtitlesEnabled = subtitlesEnabled,
    subtitleLanguage = subtitleLanguage,
    seekIntervalSeconds = normalizeSeekIntervalSeconds(seekIntervalSeconds),
    skipIntroMode = normalizeSkipIntroMode(skipIntroMode),
    speedUpCredits = speedUpCredits,
    forYouMinutes = forYouMinutes,
    homeRowOrder = homeRowOrder.decodeLineList(),
    homePinnedRows = homePinnedRows.decodeLineList(),
    homeHiddenRows = homeHiddenRows.decodeLineList(),
)

internal fun String.decodeCommaList(): List<String> =
    split(',').map(String::trim).filter(String::isNotEmpty)

internal fun String.decodeLineList(): List<String> =
    split('\n').map(String::trim).filter(String::isNotEmpty)

/**
 * Decodes the server's document, keeping this build's value for anything it does not
 * recognise or cannot read.
 *
 * [fallback] is the current local state rather than the class defaults, and that
 * distinction matters: a server response missing a key must leave that setting alone, not
 * silently reset it. That is what makes it safe for the gateway to grow a setting before
 * every television in the house has the release that knows about it.
 */
fun decodeUserPreferences(
    json: JsonObject,
    fallback: UserPreferences = UserPreferences(),
): UserPreferences = UserPreferences(
    homeSections = json.stringList("homeSections", fallback.homeSections)
        .ifEmpty { fallback.homeSections },
    homeCardDensity = json.string("homeCardDensity", fallback.homeCardDensity),
    homeArtworkStyle = json.string("homeArtworkStyle", fallback.homeArtworkStyle),
    showHomeCardMetadata = json.boolean("showHomeCardMetadata", fallback.showHomeCardMetadata),
    showRatingsStrip = json.boolean("showRatingsStrip", fallback.showRatingsStrip),
    hideWatchedMovies = json.boolean("hideWatchedMovies", fallback.hideWatchedMovies),
    showTitleLogo = json.boolean("showTitleLogo", fallback.showTitleLogo),
    welcomeQuoteStyle = json.string("welcomeQuoteStyle", fallback.welcomeQuoteStyle),
    themeId = json.string("themeId", fallback.themeId),
    autoPlayNextEpisode = json.boolean("autoPlayNextEpisode", fallback.autoPlayNextEpisode),
    showTenMinuteReminder = json.boolean("showTenMinuteReminder", fallback.showTenMinuteReminder),
    subtitlesEnabled = json.boolean("subtitlesEnabled", fallback.subtitlesEnabled),
    subtitleLanguage = json.string("subtitleLanguage", fallback.subtitleLanguage),
    // Normalised rather than trusted: the catalogue on the gateway may offer an interval
    // this build has no vocabulary for, and a skip of an unknown length is worse than the
    // default. A key the response does not carry leaves this television's value alone.
    seekIntervalSeconds = normalizeSeekIntervalSeconds(
        json.int("seekIntervalSeconds", fallback.seekIntervalSeconds),
    ),
    // Normalised for the same reason the interval above is: a mode this build has no
    // vocabulary for must cost the viewer the default button, never an unexplained jump.
    skipIntroMode = normalizeSkipIntroMode(
        json.string("skipIntroMode", fallback.skipIntroMode),
    ),
    speedUpCredits = json.boolean("speedUpCredits", fallback.speedUpCredits),
    forYouMinutes = json.int("forYouMinutes", fallback.forYouMinutes),
    homeRowOrder = json.stringList("homeRowOrder", fallback.homeRowOrder),
    homePinnedRows = json.stringList("homePinnedRows", fallback.homePinnedRows),
    homeHiddenRows = json.stringList("homeHiddenRows", fallback.homeHiddenRows),
)

/** The document as the gateway expects it. The server normalises whatever arrives. */
fun UserPreferences.encode(): JsonObject = buildJsonObject {
    putJsonArray("homeSections") { homeSections.forEach { add(JsonPrimitive(it)) } }
    put("homeCardDensity", homeCardDensity)
    put("homeArtworkStyle", homeArtworkStyle)
    put("showHomeCardMetadata", showHomeCardMetadata)
    put("showRatingsStrip", showRatingsStrip)
    put("hideWatchedMovies", hideWatchedMovies)
    put("showTitleLogo", showTitleLogo)
    put("welcomeQuoteStyle", welcomeQuoteStyle)
    put("themeId", themeId)
    put("autoPlayNextEpisode", autoPlayNextEpisode)
    put("showTenMinuteReminder", showTenMinuteReminder)
    put("subtitlesEnabled", subtitlesEnabled)
    put("subtitleLanguage", subtitleLanguage)
    put("seekIntervalSeconds", seekIntervalSeconds)
    put("skipIntroMode", skipIntroMode)
    put("speedUpCredits", speedUpCredits)
    put("forYouMinutes", forYouMinutes)
    putJsonArray("homeRowOrder") { homeRowOrder.forEach { add(JsonPrimitive(it)) } }
    putJsonArray("homePinnedRows") { homePinnedRows.forEach { add(JsonPrimitive(it)) } }
    putJsonArray("homeHiddenRows") { homeHiddenRows.forEach { add(JsonPrimitive(it)) } }
}

private fun JsonObject.string(key: String, fallback: String): String =
    (this[key] as? JsonPrimitive)?.takeIf { it.isString }?.content ?: fallback

// A JSON string is not a boolean here: "false" arriving as text must fall back rather than
// be read as some truthy value, which is why isString is checked before the conversion.
private fun JsonObject.boolean(key: String, fallback: Boolean): Boolean =
    (this[key] as? JsonPrimitive)?.takeUnless { it.isString }?.booleanOrNull ?: fallback

private fun JsonObject.int(key: String, fallback: Int): Int =
    (this[key] as? JsonPrimitive)?.takeUnless { it.isString }?.intOrNull ?: fallback

private fun JsonObject.stringList(key: String, fallback: List<String>): List<String> {
    val array = this[key] as? JsonArray ?: return fallback
    return array.mapNotNull { element ->
        (element as? JsonPrimitive)?.takeIf { it.isString }?.content?.trim()?.takeIf(String::isNotEmpty)
    }.distinct()
}

