import Foundation

/// T-iOS-23 · Pure unread-watermark bookkeeping for the session switcher.
///
/// A session shows an unread dot iff the server's `lastOutputAt` snapshot
/// (`GET /live-sessions`, T-iOS-37 optional field) is STRICTLY newer than the
/// local last-seen watermark — mirroring the web tab dot (public/tabs.ts
/// `hasActivity`: inactive tab got output since last viewed; viewing clears it).
///
/// 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` nil / non-positive → never unread**: nil 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 `maxEntries` watermarks, oldest-seen dropped first —
///   the App persists this dictionary (UserDefaults) and sessions are
///   ephemeral, so unbounded growth is a slow leak.
///
/// Persistence-agnostic pure struct (plan §7): the App layer owns loading /
/// saving `watermarks` (UserDefaults); this type never does I/O.
public struct UnreadLedger: Sendable, Equatable {
    /// Upper bound on persisted watermarks (oldest dropped beyond it).
    public static let maxEntries = 512

    /// sessionId → last-seen instant (ms since epoch, same clock as the
    /// server's `lastOutputAt`).
    public let watermarks: [UUID: Int]

    public init(watermarks: [UUID: Int] = [:]) {
        self.watermarks = Self.capped(watermarks)
    }

    /// New ledger with `sessionId` marked seen at `ms` (monotonic max — an
    /// earlier timestamp never lowers the stored watermark). The receiver is
    /// untouched (immutable style).
    public func record(seen sessionId: UUID, at ms: Int) -> UnreadLedger {
        UnreadLedger(
            watermarks: watermarks.merging([sessionId: ms]) { existing, new in
                max(existing, new)
            }
        )
    }

    /// Unread test: strictly newer server output than the local watermark.
    /// nil / non-positive `lastOutputAt` is never unread (see type doc).
    public func isUnread(sessionId: UUID, lastOutputAt: Int?) -> Bool {
        guard let lastOutputAt, lastOutputAt > 0 else { return false }
        return lastOutputAt > (watermarks[sessionId] ?? 0)
    }

    /// Keep the `maxEntries` NEWEST watermarks (deterministic tie-break by
    /// uuid string so equal timestamps cannot flap across runs).
    private static func capped(_ watermarks: [UUID: Int]) -> [UUID: Int] {
        guard watermarks.count > maxEntries else { return watermarks }
        let newestFirst = watermarks.sorted { lhs, rhs in
            lhs.value != rhs.value
                ? lhs.value > rhs.value
                : lhs.key.uuidString < rhs.key.uuidString
        }
        return Dictionary(uniqueKeysWithValues: Array(newestFirst.prefix(maxEntries)))
    }
}
