package wang.yaojia.webterm.session

/**
 * Pure unread-watermark bookkeeping for the session switcher (A6; ports iOS
 * `SessionCore.UnreadLedger`).
 *
 * A session shows an unread dot iff the server's `lastOutputAt` snapshot
 * (`GET /live-sessions`, optional field) is STRICTLY newer than the local
 * last-seen watermark — mirroring the web tab dot (public/tabs.ts `hasActivity`).
 *
 * Semantics (documented decisions):
 * - **Missing watermark = 0**: a session never seen on this device has unseen
 *   output by definition (`lastOutputAt >= createdAt > 0` server-side), so it
 *   lights up until first viewed.
 * - **`lastOutputAt` null / non-positive → never unread**: null means a pre-P1
 *   server that does not serialize the field; non-positive is malformed untrusted
 *   input — no data, no dot (plan §4: never trust, never guess).
 * - **Monotonic**: recording an EARLIER time never lowers an existing watermark
 *   (late/out-of-order callbacks cannot resurrect a dot).
 * - **Capped**: at most [MAX_ENTRIES] watermarks, oldest-seen dropped first — the
 *   App persists this map (DataStore) and sessions are ephemeral, so unbounded
 *   growth is a slow leak.
 *
 * Persistence-agnostic pure type: the App layer owns loading / saving
 * [watermarks]; this type never does I/O. Session ids are the wire's UUID-v4
 * lowercase strings (matching `ServerMessage.Attached.sessionId`).
 */
public class UnreadLedger(watermarks: Map<String, Long> = emptyMap()) {
    /** sessionId → last-seen instant (ms since epoch, same clock as `lastOutputAt`). */
    public val watermarks: Map<String, Long> = capped(watermarks)

    /**
     * New ledger with [sessionId] marked seen at [atMs] (monotonic max — an earlier
     * timestamp never lowers the stored watermark). The receiver is untouched.
     */
    public fun record(sessionId: String, atMs: Long): UnreadLedger {
        val existing = watermarks[sessionId]
        val next = if (existing != null) maxOf(existing, atMs) else atMs
        return UnreadLedger(watermarks + (sessionId to next))
    }

    /**
     * Unread test: strictly newer server output than the local watermark. null /
     * non-positive [lastOutputAt] is never unread (see type doc).
     */
    public fun isUnread(sessionId: String, lastOutputAt: Long?): Boolean {
        if (lastOutputAt == null || lastOutputAt <= 0L) return false
        return lastOutputAt > (watermarks[sessionId] ?: 0L)
    }

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is UnreadLedger) return false
        return watermarks == other.watermarks
    }

    override fun hashCode(): Int = watermarks.hashCode()

    override fun toString(): String = "UnreadLedger(watermarks=$watermarks)"

    public companion object {
        /** Upper bound on persisted watermarks (oldest dropped beyond it). */
        public const val MAX_ENTRIES: Int = 512

        /**
         * Keep the [MAX_ENTRIES] NEWEST watermarks (deterministic tie-break by
         * sessionId string so equal timestamps cannot flap across runs).
         */
        private fun capped(watermarks: Map<String, Long>): Map<String, Long> {
            if (watermarks.size <= MAX_ENTRIES) return watermarks
            return watermarks.entries
                .sortedWith(
                    compareByDescending<Map.Entry<String, Long>> { it.value }
                        .thenBy { it.key },
                )
                .take(MAX_ENTRIES)
                .associate { it.key to it.value }
        }
    }
}
