package wang.yaojia.webterm.api.models

import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.booleanOrNull

/**
 * The cross-device Projects-UI prefs blob (`GET /prefs` RO · `PUT /prefs` G) — an
 * OPAQUE-BUT-VALIDATED JSON object round-trip. Known keys mirror the web client
 * (`favourites: string[]`, `collapsed: { group-key: true }`); ALL OTHER top-level keys are
 * preserved verbatim across decode → mutate → encode, so an Android `PUT` can never clobber prefs
 * written by the web/iOS client or a future server (`PUT` replaces the WHOLE blob server-side, so
 * key preservation is correctness, not politeness).
 *
 * Immutable snapshot: [withFavourites]/[withCollapsed] return NEW copies that replace exactly one
 * known key and carry every other key through untouched. Numbers round-trip verbatim because a
 * parsed [JsonPrimitive] keeps its source token (`42` never re-encodes as `42.0`).
 */
public class UiPrefs private constructor(private val storage: JsonObject) {

    /**
     * Favourited project paths (★): non-empty JSON strings, de-duplicated, original order kept.
     * Wrong-typed entries are dropped, not fatal (mirrors `public/prefs.ts sanitizePrefs`).
     */
    public val favourites: List<String>
        get() {
            val array = storage[KEY_FAVOURITES] as? JsonArray ?: return emptyList()
            val out = LinkedHashSet<String>()
            for (element in array) {
                val primitive = element as? JsonPrimitive ?: continue
                if (!primitive.isString) continue
                val path = primitive.content
                if (path.isNotEmpty()) out.add(path)
            }
            return out.toList()
        }

    /**
     * Namespace group-key → collapsed. Only literal `true` values count (expanded is the default;
     * both web and server sanitizers agree). A JSON string `"true"` does NOT count.
     */
    public val collapsed: Map<String, Boolean>
        get() {
            val obj = storage[KEY_COLLAPSED] as? JsonObject ?: return emptyMap()
            val out = LinkedHashMap<String, Boolean>()
            for ((key, value) in obj) {
                if (key.isEmpty()) continue
                if (value is JsonPrimitive && !value.isString && value.booleanOrNull == true) {
                    out[key] = true
                }
            }
            return out
        }

    /** New snapshot with `favourites` replaced; every other key untouched (position preserved). */
    public fun withFavourites(favourites: List<String>): UiPrefs {
        val next = LinkedHashMap<String, JsonElement>(storage)
        next[KEY_FAVOURITES] = JsonArray(favourites.map { JsonPrimitive(it) })
        return UiPrefs(JsonObject(next))
    }

    /** New snapshot with `collapsed` replaced; every other key untouched (position preserved). */
    public fun withCollapsed(collapsed: Map<String, Boolean>): UiPrefs {
        val next = LinkedHashMap<String, JsonElement>(storage)
        next[KEY_COLLAPSED] = JsonObject(collapsed.mapValues { JsonPrimitive(it.value) })
        return UiPrefs(JsonObject(next))
    }

    /** Encode the FULL blob (known + unknown keys) for `PUT /prefs`. */
    public fun encodeBody(): ByteArray =
        ModelJson.encodeToString(JsonObject.serializer(), storage).encodeToByteArray()

    override fun equals(other: Any?): Boolean = other is UiPrefs && other.storage == storage
    override fun hashCode(): Int = storage.hashCode()
    override fun toString(): String = "UiPrefs(storage=$storage)"

    public companion object {
        private const val KEY_FAVOURITES = "favourites"
        private const val KEY_COLLAPSED = "collapsed"

        /** Fresh prefs (e.g. first write from a device that never fetched). */
        public fun create(
            favourites: List<String> = emptyList(),
            collapsed: Map<String, Boolean> = emptyMap(),
        ): UiPrefs = UiPrefs(
            JsonObject(
                mapOf(
                    KEY_FAVOURITES to JsonArray(favourites.map { JsonPrimitive(it) }),
                    KEY_COLLAPSED to JsonObject(collapsed.mapValues { JsonPrimitive(it.value) }),
                ),
            ),
        )

        /**
         * Decode a `/prefs` body. `null` = top level is not a JSON object — callers surface
         * `InvalidResponseBody` LOUDLY instead of degrading to empty prefs (an empty-based `PUT`
         * would wipe the server blob).
         */
        public fun decode(bytes: ByteArray): UiPrefs? {
            val element = runCatching { ModelJson.parseToJsonElement(bytes.decodeToString()) }.getOrNull()
            return (element as? JsonObject)?.let { UiPrefs(it) }
        }
    }
}
