import Foundation public enum WatchBaselineMaturity: String, Codable, Sendable { case unavailable case provisional case personalized } public enum WatchBaselineComparison: String, Codable, Sendable { case unavailable case usual case slightlyDifferent case clearlyDifferent } public struct WatchBaselineSnapshot: Codable, Equatable, Sendable { public let maturity: WatchBaselineMaturity public let comparableMeasurements: Int public let medianBPM: Double? public let medianAbsoluteDeviation: Double? public init(measurements: [WatchMeasurementSummaryV1]) { let values = measurements .filter { $0.source == .appleWatchOptical && $0.isValid && $0.quality >= 0.65 && $0.context.isBaselineComparable } .compactMap(\.averageBPM) .filter(\.isFinite) comparableMeasurements = values.count medianBPM = Self.median(values) medianAbsoluteDeviation = medianBPM.map { median in Self.median(values.map { abs($0 - median) }) ?? 0 } if values.count >= 10 { maturity = .personalized } else if values.count >= 5 { maturity = .provisional } else { maturity = .unavailable } } public func compare(bpm: Double?) -> WatchBaselineComparison { guard maturity != .unavailable, let bpm, bpm.isFinite, let medianBPM else { return .unavailable } let scale = max(medianAbsoluteDeviation ?? 0, 2) let robustDistance = abs(bpm - medianBPM) / scale if robustDistance < 2 { return .usual } if robustDistance < 4 { return .slightlyDifferent } return .clearlyDifferent } private static func median(_ values: [Double]) -> Double? { guard !values.isEmpty else { return nil } let sorted = values.sorted() let middle = sorted.count / 2 return sorted.count.isMultiple(of: 2) ? (sorted[middle - 1] + sorted[middle]) / 2 : sorted[middle] } } public extension WatchBaselineComparison { var frenchLabel: String { switch self { case .unavailable: "Baseline en construction" case .usual: "Habituel" case .slightlyDifferent: "Légèrement différent" case .clearlyDifferent: "Clairement différent" } } }