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

import com.runicgateway.app.data.api.dto.BrandDto
import com.runicgateway.app.data.api.dto.SettingsDto
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive

/**
 * Everything the app renders itself with that the shard's admin controls
 * (THEMING_AND_NAV.md, M12): the brand block, the resolved theme tokens, and the
 * public navigation overrides. One value, held once in [com.runicgateway.app.ui.AppViewModel],
 * so the theme and the drawer can never disagree about which shard they are showing.
 *
 * **[NONE] is the shipped app.** An instance with no settings rows, a backend
 * that predates the feature, and a settings call that failed outright are all the
 * same state here, and all three must render exactly as the app did before this
 * milestone existed (§2). That is why nothing on this class is nullable except
 * [brand], which was already nullable and whose absence already meant "use the
 * bundled strings".
 */
data class SiteAppearance(
    /** The per-shard branding block; null when settings couldn't be loaded. */
    val brand: BrandDto? = null,
    /**
     * The resolved CSS custom properties, keyed by token (`"--accent"` → `"#7f99bd"`).
     * Empty means "the shipped defaults" — the server never emits an empty map,
     * but absent and empty are the same thing to the app and it must not depend
     * on that.
     */
    val theme: Map<String, String> = emptyMap(),
    /**
     * The parsed `nav_public` row, or null when the admin never edited the nav.
     * Kept as the raw object here; reading `items` / `sections` / `links` out of
     * it is the job of the phases that render them.
     */
    val navPublic: JsonObject? = null,
) {
    companion object {
        /** The shipped app: no brand, no overrides. Also what a failed load means. */
        val NONE = SiteAppearance()

        /**
         * Build the appearance from a `GET /public/settings` body. Forgiving
         * field by field (§2): a bad `--accent` must not discard a good `--bg`
         * beside it, and a malformed `nav_public` must not cost the theme.
         */
        fun from(settings: SettingsDto?): SiteAppearance {
            if (settings == null) return NONE
            return SiteAppearance(
                brand = settings.brand,
                theme = themeTokens(settings.theme as? JsonObject),
                navPublic = parseJsonSetting(settings.navPublic),
            )
        }

        // Every themable token is a string server-side (validated on write, and
        // resolveThemeTokens only ever copies a validated value). Anything else
        // is dropped rather than coerced, so an unexpected value costs exactly
        // its own token and the rest of the palette still applies.
        private fun themeTokens(raw: JsonObject?): Map<String, String> {
            if (raw.isNullOrEmpty()) return emptyMap()
            return buildMap {
                for ((token, value) in raw) {
                    val text = (value as? JsonPrimitive)?.takeIf { it.isString }?.content
                    if (!text.isNullOrBlank()) put(token, text)
                }
            }
        }
    }
}
