HealthProbe / HealthProbe / ViewModels / DataTypesViewModel.swift
1 contributor
130 lines | 5.213kb
import Foundation

struct DataTypeSnapshotContext: Equatable, Sendable {
    let observationID: Int64
    let observedAt: Date
}

@Observable
final class DataTypesViewModel {
    var filter: DiffFilter = .all
    var comparisonMode: ComparisonMode = .previous
    var observationRows: [CachedArchiveObservationRow] = []
    var observationRowsError: String?
    var archiveDiffs: [TypeDiff]?
    var archiveDiffError: String?

    var observationContexts: [DataTypeSnapshotContext] {
        observationRows
            .sorted { $0.observedAt > $1.observedAt }
            .map {
                DataTypeSnapshotContext(
                    observationID: $0.observationID,
                    observedAt: $0.observedAt
                )
            }
    }

    func diffs() -> [TypeDiff] {
        guard let archiveDiffs else { return [] }
        return apply(filter: filter, to: archiveDiffs)
    }

    func baseline(for snapshot: DataTypeSnapshotContext, in snapshots: [DataTypeSnapshotContext]) -> DataTypeSnapshotContext? {
        resolveBaseline(for: snapshot, in: snapshots)
    }

    func clearArchiveDiffs() {
        archiveDiffs = nil
        archiveDiffError = nil
    }

    @MainActor
    func loadArchiveRows(limit: Int = 200) async {
        do {
            let cache = try CoreDataArchiveCacheStore()
            observationRows = try cache.observationRows(limit: limit)
            observationRowsError = nil
        } catch {
            observationRows = []
            observationRowsError = error.localizedDescription
        }
    }

    @MainActor
    func loadArchiveDiffs(current: DataTypeSnapshotContext?, snapshots: [DataTypeSnapshotContext]) async {
        guard let current,
              let baseline = resolveBaseline(for: current, in: snapshots) else {
            clearArchiveDiffs()
            return
        }

        do {
            let cache = try CoreDataArchiveCacheStore()
            let currentSummaries = try cache.typeSummaries(observationID: current.observationID)
            let baselineSummaries = try cache.typeSummaries(observationID: baseline.observationID)
            let currentByType = Dictionary(uniqueKeysWithValues: currentSummaries.map { ($0.sampleTypeIdentifier, $0) })
            let baselineByType = Dictionary(uniqueKeysWithValues: baselineSummaries.map { ($0.sampleTypeIdentifier, $0) })
            let allTypeIdentifiers = Set(currentByType.keys).union(baselineByType.keys)

            var archiveRows: [TypeDiff] = []
            archiveRows.reserveCapacity(allTypeIdentifiers.count)

            for typeIdentifier in allTypeIdentifiers {
                let summary = currentByType[typeIdentifier]
                let baselineSummary = baselineByType[typeIdentifier]
                let diff = try await SQLiteHealthArchiveStore.shared.diffSummary(HealthArchiveDiffRequest(
                    fromObservationID: baseline.observationID,
                    toObservationID: current.observationID,
                    sampleTypeIdentifier: typeIdentifier
                ))
                archiveRows.append(TypeDiff(
                    id: typeIdentifier,
                    typeIdentifier: typeIdentifier,
                    displayName: summary?.displayName ?? baselineSummary?.displayName ?? typeIdentifier,
                    currentCount: summary?.visibleRecordCount ?? 0,
                    previousCount: baselineSummary?.visibleRecordCount ?? 0,
                    previousTracked: baselineSummary != nil,
                    appearedCount: diff.appearedCount,
                    disappearedCount: diff.disappearedCount,
                    representationChangedCount: diff.representationChangedCount
                ))
            }

            archiveDiffs = archiveRows.sorted {
                $0.displayName.localizedCompare($1.displayName) == .orderedAscending
            }
            archiveDiffError = nil
        } catch {
            archiveDiffs = nil
            archiveDiffError = error.localizedDescription
        }
    }

    private func resolveBaseline(
        for snapshot: DataTypeSnapshotContext,
        in snapshots: [DataTypeSnapshotContext]
    ) -> DataTypeSnapshotContext? {
        let sorted = snapshots.sorted { $0.observedAt > $1.observedAt }
        switch comparisonMode {
        case .previous:
            return sorted.first { $0.observedAt < snapshot.observedAt }
        case .selected:
            return nil  // DataTypesView uses .previous by default; selection lives in SnapshotsTab
        case .relativeTime(let interval):
            let target = snapshot.observedAt.addingTimeInterval(-interval)
            return snapshots
                .filter { $0.observedAt <= target }
                .max { $0.observedAt < $1.observedAt }
        }
    }

    private func apply(filter: DiffFilter, to diffs: [TypeDiff]) -> [TypeDiff] {
        switch filter {
        case .all:       return diffs
        case .changed:   return diffs.filter { $0.previousTracked && $0.hasChanges }
        case .increased: return diffs.filter { $0.previousTracked && $0.delta > 0 }
        case .decreased: return diffs.filter { $0.previousTracked && $0.delta < 0 }
        }
    }
}