import Foundation

/// One activity-timeline entry from `GET /live-sessions/:id/events` (frozen
/// contract, plan §3.1; mirrors src/types.ts:428-433). The server derives
/// `class` semantically (src/types.ts:423: tool/waiting/done/stuck/user) — the
/// client never re-derives it from hook names. `class` stays a raw `String` so
/// the SHAPE decodes even for future/unknown classes; consumers drop unknowns
/// (use `decodeList`, which does exactly that).
public struct TimelineEvent: Sendable, Equatable, Decodable {
    /// Server ingest timestamp (ms since epoch).
    public let at: Int
    /// Server-derived semantic class; see `knownClasses`.
    public let `class`: String
    /// Sanitized tool name (server-side: ≤200 chars, control chars stripped).
    public let toolName: String?
    /// Server-derived human phrase ("ran Bash", "edited 3 files").
    public let label: String

    /// The server's `TimelineClass` union (src/types.ts:423).
    public static let knownClasses: Set<String> = ["tool", "waiting", "done", "stuck", "user"]

    public init(at: Int, class className: String, toolName: String?, label: String) {
        self.at = at
        self.`class` = className
        self.toolName = toolName
        self.label = label
    }

    /// True when `class` is one the server currently emits.
    public var hasKnownClass: Bool {
        Self.knownClasses.contains(`class`)
    }

    /// Decode a `/events` response body. NEVER throws: the server is an
    /// untrusted input source — malformed entries and unknown-class entries are
    /// silently dropped; a non-array body yields `[]`.
    public static func decodeList(from data: Data) -> [TimelineEvent] {
        guard let entries = try? JSONDecoder().decode([LossyEntry].self, from: data) else {
            return []
        }
        return entries.compactMap(\.value).filter(\.hasKnownClass)
    }
}

/// Per-element tolerance shim: a malformed element becomes `nil` instead of
/// failing the whole array decode.
private struct LossyEntry: Decodable {
    let value: TimelineEvent?

    init(from decoder: any Decoder) {
        value = try? TimelineEvent(from: decoder)
    }
}
