360feba 12 hours ago History
1 contributor
1476 lines | 69.604kb
import SwiftUI
import HealthKit
import UIKit

struct DashboardView: View {
    @Environment(AppSettings.self) private var appSettings
    @State private var viewModel = DashboardViewModel()
    @State private var currentDeviceProfile = LocalDeviceProfileStore.profile(for: AppSettings.currentDeviceID)
    @State private var didAutoRequestPermissions = false
    @State private var snapshotSheetTab: SnapshotSheetTab = .progress
    @State private var diagnosticReport: DiagnosticReport?
    @State private var expandedIssueIDs: Set<String> = []
    @State private var idleTimerWasDisabledBeforeSnapshot = false
    @State private var snapshotIdleTimerOverrideActive = false
    @State private var savedDiagnosticSnapshotIDs: Set<UUID> = []

    private var latestArchiveObservation: CachedArchiveObservationRow? {
        viewModel.latestArchiveObservation
    }
    private var previousArchiveObservation: CachedArchiveObservationRow? {
        guard viewModel.archiveObservationRows.count > 1 else { return nil }
        return viewModel.archiveObservationRows[1]
    }
    private var currentDeviceDisplayName: String {
        if !currentDeviceProfile.name.isEmpty { return currentDeviceProfile.name }
        return "Local device"
    }

    private var latestArchiveChangeCount: Int? {
        guard let latestArchiveObservation,
              previousArchiveObservation != nil else {
            return nil
        }
        return latestArchiveObservation.appearedCount
            + latestArchiveObservation.disappearedCount
            + latestArchiveObservation.representationChangedCount
    }

    var body: some View {
        NavigationStack {
            List {
                statusSection
                actionsSection
                if let msg = viewModel.authError ?? viewModel.snapshotError {
                    Section {
                        Label(msg, systemImage: "exclamationmark.circle")
                            .foregroundStyle(Color.criticalRed)
                            .font(.caption)
                    }
                }
            }
            .navigationTitle("HealthProbe")
        }
        .sheet(isPresented: $viewModel.showProgressSheet) {
            progressSheet
        }
        .task {
            currentDeviceProfile = LocalDeviceProfileStore.profile(for: AppSettings.currentDeviceID)
            await viewModel.loadArchiveCacheStatus()
            if !didAutoRequestPermissions && !HealthKitService.shared.hasRequestedPermissionsBefore {
                didAutoRequestPermissions = true
                await viewModel.requestAuthorization()
            }
        }
        .onChange(of: viewModel.snapshotProgress) { _, newProgress in
            updateIdleTimer(for: newProgress)
        }
        .onDisappear {
            restoreSnapshotIdleTimerIfNeeded()
        }
    }

    // MARK: - Helpers

    private func updateIdleTimer(for progress: SnapshotProgress) {
        let shouldDisableIdleTimer = progress == .fetching || progress == .processing

        if shouldDisableIdleTimer && !snapshotIdleTimerOverrideActive {
            idleTimerWasDisabledBeforeSnapshot = UIApplication.shared.isIdleTimerDisabled
            UIApplication.shared.isIdleTimerDisabled = true
            snapshotIdleTimerOverrideActive = true
        } else if !shouldDisableIdleTimer {
            restoreSnapshotIdleTimerIfNeeded()
        }
    }

    private func restoreSnapshotIdleTimerIfNeeded() {
        guard snapshotIdleTimerOverrideActive else { return }
        UIApplication.shared.isIdleTimerDisabled = idleTimerWasDisabledBeforeSnapshot
        snapshotIdleTimerOverrideActive = false
    }

    // MARK: - Report helpers

    private func failedTypesList(in progress: SnapshotFetchProgress) -> [SnapshotFetchProgress.TypeProgress] {
        progress.visibleTypes.filter {
            if case .failed = $0.status { return true }
            return false
        }
    }

    private func timedOutMetricNames(_ progress: SnapshotFetchProgress?) -> [String] {
        progress?.types.compactMap { type in
            if case .failed(let r) = type.status, r == "Timeout" { return type.displayName }
            return nil
        } ?? []
    }

    private func hasTimedOutMetrics(_ progress: SnapshotFetchProgress?) -> Bool {
        progress?.types.contains {
            if case .failed(let reason) = $0.status { return reason == "Timeout" }
            return false
        } ?? false
    }

    private func hasAuthorizationFailures(_ progress: SnapshotFetchProgress?) -> Bool {
        progress?.types.contains {
            if case .failed(let reason) = $0.status { return reason == "Not authorized" }
            return false
        } ?? false
    }

    private func degradedTypesList(in progress: SnapshotFetchProgress) -> [SnapshotFetchProgress.TypeProgress] {
        progress.types.filter { type in
            let hasAllExpectedCalls = Set(type.apiCallDetails.map(\.queryType)) == Set(["record_import", "earliest_sample", "latest_sample"])
            let allCallsComplete = type.apiCallDetails.allSatisfy { $0.status == .complete }
            return type.quality != "complete" || !hasAllExpectedCalls || !allCallsComplete
        }
    }

    private func formatDuration(_ seconds: TimeInterval) -> String {
        if seconds < 60 { return String(format: "%.1fs", seconds) }
        return "\(Int(seconds) / 60)m \(Int(seconds) % 60)s"
    }

    private func timingResidual(totalElapsed: TimeInterval, breakdown: ImportTimingBreakdown) -> TimeInterval {
        max(0, totalElapsed - breakdown.accountedElapsedSeconds)
    }

    private func qualityLabel(progress: SnapshotFetchProgress) -> String {
        switch viewModel.snapshotProgress {
        case .complete: return "Complete"
        case .requiresResolution: return "Needs decision"
        case .incomplete:
            let hasUnauthorized = failedTypesList(in: progress).contains {
                if case .failed(let r) = $0.status { return r == "Not authorized" }
                return false
            }
            return hasUnauthorized ? "Partial (unauthorized)" : "Partial"
        default: return "—"
        }
    }

    private func qualityColor(progress: SnapshotFetchProgress) -> Color {
        switch viewModel.snapshotProgress {
        case .complete:   return .healthyGreen
        case .requiresResolution: return .warningAmber
        case .incomplete: return .warningAmber
        default:          return .neutralGray
        }
    }

    private func failureReason(_ type: SnapshotFetchProgress.TypeProgress) -> String {
        if case .failed(let r) = type.status { return r.isEmpty ? "Failed" : r }
        return "Unknown"
    }

    private func failureImpact(_ reason: String) -> String {
        switch reason {
        case "Not authorized": return "Excluded from checksum and change analysis"
        case "Timeout":        return "Data unavailable — type skipped this run"
        case "Unsupported":    return "Not supported on this device or OS version"
        default:               return "Data unavailable"
        }
    }

    private func remediationNoteItems(progress: SnapshotFetchProgress) -> [String] {
        let failed = failedTypesList(in: progress)
        var items: [String] = []
        let unauthorized = failed.filter {
            if case .failed(let r) = $0.status { return r == "Not authorized" }
            return false
        }
        let timedOut = failed.filter {
            if case .failed(let r) = $0.status { return r == "Timeout" }
            return false
        }
        if !unauthorized.isEmpty {
            items.append("Open iOS Settings, choose HealthProbe, and re-enable Health read access for the listed metrics.")
            items.append("After restoring access, create a new snapshot before relying on checksum or change analysis.")
        }
        if !timedOut.isEmpty {
            items.append("Timeout reflects HealthProbe's configured limit, not missing Health data or permission.")
        }
        if viewModel.snapshotProgress == .incomplete {
            items.append("Partial snapshots are excluded from change analysis. A complete snapshot is needed to resume integrity checks.")
        }
        return items
    }

    private func ambiguousDisappearanceNotes() -> [String] {
        [
            "All values are missing for the listed metric in the current Health view. Apple API does not allow HealthProbe to verify Health read authorization status.",
            "Possible causes: Health read authorization was withdrawn, or the metric is genuinely absent from the current Health store."
        ]
    }

    private func ambiguousDisappearanceActions() -> [String] {
        [
            "Treat the metric as unauthorized.",
            "Treat the metric as absent from the current Health store.",
            "Cancel saving this snapshot."
        ]
    }

    private func iso8601String(_ date: Date?) -> String {
        guard let date else { return "none" }
        return ISO8601DateFormatter().string(from: date)
    }

    private enum DiagnosticReportMode {
        case compact
        case full
    }

    private func operationResultValue() -> String {
        switch viewModel.snapshotProgress {
        case .complete:
            return "complete_success"
        case .incomplete:
            return "partial_success"
        case .requiresResolution:
            return "requires_user_resolution"
        case .idle, .fetching, .processing:
            return "failed"
        }
    }

    private func normalizedAuthorization(for type: SnapshotFetchProgress.TypeProgress) -> (status: String, source: String) {
        let hasSuccessfulQuery = type.apiCallDetails.contains { $0.status == .complete }
        let normalized = type.authorizationStatus.lowercased()

        if hasSuccessfulQuery {
            return ("granted", "inferredFromSuccessfulQuery")
        }

        if normalized == "denied" {
            return ("denied", "inferredFromUnauthorizedQuery")
        }

        if normalized == "notdetermined" {
            return ("notDetermined", "reported")
        }

        if normalized == "granted" {
            return ("granted", "reported")
        }

        // When real HK authorization status is unavailable, report unknown+unavailable.
        return ("unknown", "unavailable")
    }

    private func metricDateString(_ date: Date?, queryType: String, calls: [HealthKitAPICallResult]) -> String {
        if let date { return iso8601String(date) }
        guard let call = calls.first(where: { $0.queryType == queryType }) else { return "unknown" }
        return call.status == .complete ? "none" : "unknown"
    }

    private func yearlyCountsString(_ counts: [SnapshotFetchProgress.YearlyCountProgress]) -> String {
        guard !counts.isEmpty else { return "none" }
        return counts
            .sorted { $0.year < $1.year }
            .map { "\($0.year)=\($0.count)\($0.isApproximate ? " approximate" : "")" }
            .joined(separator: ", ")
    }

    private func callResultLabel(_ call: HealthKitAPICallResult) -> String {
        if let result = call.resultValue, !result.isEmpty {
            return result
        }
        return call.status == .complete ? "none" : "unknown"
    }

    private func shouldPrintCallErrorFields(_ call: HealthKitAPICallResult) -> Bool {
        switch call.status {
        case .failed, .timeout, .unknown, .unauthorized:
            return true
        case .complete, .unsupported:
            return false
        }
    }

    private func buildDiagnosticText(_ progress: SnapshotFetchProgress, mode: DiagnosticReportMode = .full) -> String {
        let snapshotID = viewModel.completedSnapshotID.map { $0.uuidString } ?? "unknown"
        let deviceID   = viewModel.completedSnapshotDeviceID ?? "unknown"
        let timestamp  = viewModel.completedSnapshotTimestamp ?? Date()
        let operationID = viewModel.operationID?.uuidString ?? "unknown"
        let reportGeneratedAt = iso8601String(Date())
        let trigger = viewModel.completedSnapshotTriggerReason ?? "manual"
        let quality: String
        switch viewModel.snapshotProgress {
        case .complete:   quality = "complete"
        case .incomplete: quality = "partial"
        case .requiresResolution: quality = "requires_resolution"
        default:          quality = "unknown"
        }
        let duration = viewModel.fetchDurationSeconds.map { formatDuration($0) } ?? "unknown"
        let fmt = DateFormatter()
        fmt.dateFormat = "yyyy-MM-dd HH:mm:ss"
        fmt.locale = Locale(identifier: "en_US_POSIX")
        let tsStr = fmt.string(from: timestamp)
        let degraded = degradedTypesList(in: progress)
        let isPartialSnapshot = viewModel.snapshotProgress == .incomplete
        let requiresResolution = viewModel.snapshotProgress == .requiresResolution
        let failedLines: String
        if degraded.isEmpty {
            failedLines = "FAILED/DEGRADED METRICS: none"
        } else {
            failedLines = (["FAILED/DEGRADED METRICS"] + degraded
                .map { "  \($0.displayName): \($0.quality)" })
                .joined(separator: "\n")
        }

        var lines: [String] = []
        lines.append("BEGIN HEALTHPROBE REPORT")
        lines.append("")
        lines.append("OPERATION METADATA")
        lines.append("operationType: \(trigger == "retryFailedMetrics" ? "retryFailedMetrics" : "createSnapshot")")
        lines.append("operationID: \(operationID)")
        lines.append("operationResult: \(operationResultValue())")
        lines.append("primaryObjectType: snapshot")
        lines.append("primaryObjectID: \(snapshotID)")
        lines.append("reportSchemaVersion: 3")
        lines.append("reportGeneratedAt: \(reportGeneratedAt)")
        lines.append("appVersion: \(Bundle.main.appVersion)")
        lines.append("buildFingerprint: \(Bundle.main.buildFingerprint)")
        lines.append("sourceCommit: \(Bundle.main.sourceCommit)")
        lines.append("sourceDirty: \(Bundle.main.sourceDirty)")
        lines.append("")
        lines.append("OPERATION SUMMARY")
        lines.append("Snapshot:   \(snapshotID)")
        lines.append("Archive Observation: \(viewModel.completedArchiveObservationID.map(String.init) ?? "unknown")")
        lines.append("Device:     \(deviceID)")
        lines.append("Timestamp:  \(tsStr)")
        lines.append("Quality:    \(quality)")
        lines.append("Duration:   \(duration)")
        lines.append("Trigger:    \(trigger)")
        if let retryOfSnapshotID = viewModel.completedSnapshotRetryOfSnapshotID {
            lines.append("Retry of:   \(retryOfSnapshotID.uuidString)")
        }
        lines.append("")
        lines.append("DIAGNOSTIC NOTES")
        lines.append("Per-metric timing sums are cumulative work and can exceed wall-clock duration because type fetches overlap")
        lines.append("Fetch/processing/insert/finalize fields are measured for import performance comparison")
        if isPartialSnapshot {
            lines.append("Partial snapshot: true")
            lines.append("Failed metrics are excluded from checksum/change analysis")
            lines.append("Do not infer deletion from partial snapshots")
        }
        if requiresResolution {
            lines.append("Requires user resolution: true")
            lines.append("All-values-missing metrics require review before saving")
        }
        if progress.types.contains(where: { $0.apiCallDetails.contains(where: { $0.status == .timeout }) }) {
            lines.append("Timeout means HealthProbe cancelled after configured timeout, not necessarily HealthKit denial")
        }
        lines.append("")
        lines.append("CONFIGURATION")
        lines.append("adaptiveTimeoutsEnabled: \(progress.adaptiveTimeoutsEnabled ? "true" : "false")")
        lines.append("defaultInitialTimeout:   \(formatDuration(LocalMetricTimeoutProfile.defaultInitialTimeout))")
        lines.append("maximumTimeout:          \(formatDuration(LocalMetricTimeoutProfile.maximumTimeout))")
        lines.append("maxConcurrentTypeFetches: \(progress.maxConcurrentTypeFetches)")
        lines.append("")
        lines.append("CHAIN/INTEGRITY CONTEXT")
        lines.append("previousSnapshotID:      \(progress.previousSnapshotID.map { $0.uuidString } ?? "none")")
        lines.append("isChainStart:            \(progress.isChainStart.map { $0 ? "true" : "false" } ?? "unknown")")
        lines.append("snapshotChecksum:        \(progress.snapshotChecksum)")
        lines.append("monitoredTypeSetHash:    \(progress.monitoredTypeSetHash)")
        lines.append("monitoredRegistryVersion: \(progress.monitoredRegistryVersion.map(String.init) ?? "unknown")")
        lines.append("")
        lines.append("STATISTICS")
        lines.append("Records:    \(progress.totalRecords)")
        lines.append("Types:      \(progress.types.count)/\(progress.totalTypeCount) processed, \(progress.completedCount) complete, \(degraded.count) degraded")
        lines.append("WallClockDuration: \(duration)")
        let aggregateTiming = progress.aggregateTimingBreakdown
        let summedMetricTotalElapsed = progress.types.reduce(0) { $0 + max(0, $1.totalElapsedSeconds) }
        let summedMetricResidualElapsed = progress.types.reduce(0) { partial, type in
            partial + timingResidual(totalElapsed: type.totalElapsedSeconds, breakdown: type.timingBreakdown)
        }
        let captureModeCounts = Dictionary(grouping: progress.types, by: \.captureMode)
            .mapValues(\.count)
        let summedDeltaEventCount = progress.types.reduce(0) { $0 + max(0, $1.deltaEventCount) }
        lines.append("SummedMetricTotalElapsed: \(formatDuration(summedMetricTotalElapsed))")
        lines.append("SummedFetchElapsed: \(formatDuration(aggregateTiming.fetchElapsedSeconds))")
        lines.append("SummedProcessingElapsed: \(formatDuration(aggregateTiming.processingElapsedSeconds))")
        if aggregateTiming.processingAccountedElapsedSeconds > 0 {
            lines.append("SummedProcessingDeltaApplyElapsed: \(formatDuration(aggregateTiming.processingDeltaApplyElapsedSeconds))")
            lines.append("SummedProcessingRecordArchiveRebuildElapsed: \(formatDuration(aggregateTiming.processingRecordArchiveRebuildElapsedSeconds))")
            lines.append("SummedProcessingInitialRecordElapsed: \(formatDuration(aggregateTiming.processingInitialRecordElapsedSeconds))")
            lines.append("SummedProcessingRecordArchiveFinalizeElapsed: \(formatDuration(aggregateTiming.processingRecordArchiveFinalizeElapsedSeconds))")
            lines.append("SummedProcessingOtherElapsed: \(formatDuration(aggregateTiming.processingOtherElapsedSeconds))")
        }
        lines.append("SummedInsertElapsed: \(formatDuration(aggregateTiming.insertElapsedSeconds))")
        lines.append("SummedFinalizeElapsed: \(formatDuration(aggregateTiming.finalizeElapsedSeconds))")
        if aggregateTiming.finalizeAccountedElapsedSeconds > 0 {
            lines.append("SummedFinalizeEventCountElapsed: \(formatDuration(aggregateTiming.finalizeEventCountElapsedSeconds))")
            lines.append("SummedFinalizeTypeSummaryElapsed: \(formatDuration(aggregateTiming.finalizeTypeSummaryElapsedSeconds))")
            lines.append("SummedFinalizeDailyAggregateElapsed: \(formatDuration(aggregateTiming.finalizeDailyAggregateElapsedSeconds))")
            if aggregateTiming.finalizeDailyAggregateAccountedElapsedSeconds > 0 {
                lines.append("SummedFinalizeDailyAggregateBucketLookupElapsed: \(formatDuration(aggregateTiming.finalizeDailyAggregateBucketLookupElapsedSeconds))")
                lines.append("SummedFinalizeDailyAggregateCopyElapsed: \(formatDuration(aggregateTiming.finalizeDailyAggregateCopyElapsedSeconds))")
                lines.append("SummedFinalizeDailyAggregateDeleteElapsed: \(formatDuration(aggregateTiming.finalizeDailyAggregateDeleteElapsedSeconds))")
                lines.append("SummedFinalizeDailyAggregateRebuildElapsed: \(formatDuration(aggregateTiming.finalizeDailyAggregateRebuildElapsedSeconds))")
                lines.append("SummedFinalizeDailyAggregateInsertElapsed: \(formatDuration(aggregateTiming.finalizeDailyAggregateInsertElapsedSeconds))")
                lines.append("SummedFinalizeDailyAggregateOtherElapsed: \(formatDuration(aggregateTiming.finalizeDailyAggregateOtherElapsedSeconds))")
            }
            lines.append("SummedFinalizeRunUpdateElapsed: \(formatDuration(aggregateTiming.finalizeRunUpdateElapsedSeconds))")
            lines.append("SummedFinalizeOtherElapsed: \(formatDuration(aggregateTiming.finalizeOtherElapsedSeconds))")
        }
        lines.append("SummedResidualElapsed: \(formatDuration(summedMetricResidualElapsed))")
        lines.append("CaptureModes: unchangedDelta=\(captureModeCounts["unchangedDelta", default: 0]), delta=\(captureModeCounts["delta", default: 0]), initialImport=\(captureModeCounts["initialImport", default: 0]), unavailable=\(captureModeCounts["unavailable", default: 0])")
        lines.append("DeltaEvents: \(summedDeltaEventCount)")
        lines.append("")
        lines.append(failedLines)

        if requiresResolution {
            lines.append("")
            lines.append("AMBIGUOUS TOTAL DISAPPEARANCE")
            for metric in viewModel.ambiguousDisappearedMetrics {
                lines.append("  \(metric.displayName): previousCount=\(metric.previousCount), currentCount=0")
            }
            lines.append("  note: Apple API does not expose read authorization status for this decision point")
        }

        lines.append("")
        lines.append("IMPORT PERFORMANCE BY METRIC")
        let orderedTypes = progress.types.sorted(by: { $0.displayName < $1.displayName })
        let degradedIDs = Set(degraded.map(\.id))
        let reportTypes: [SnapshotFetchProgress.TypeProgress]
        switch mode {
        case .compact:
            reportTypes = orderedTypes.filter { degradedIDs.contains($0.id) }
        case .full:
            reportTypes = orderedTypes
        }

        for type in reportTypes {
            lines.append("")
            lines.append("\(type.displayName):")
            lines.append("  identifier: \(type.id)")
            lines.append("  quality: \(type.quality)")
            lines.append("  count: \(type.recordCount)")
            lines.append("  captureMode: \(type.captureMode)")
            lines.append("  deltaEvents: \(type.deltaEventCount)")
            lines.append("  timeoutMode: \(type.timeoutMode)")
            lines.append("  timeoutConfigured: \(formatDuration(type.timeoutConfiguredSeconds))")
            lines.append("  lastSuccessfulElapsed: \(type.lastSuccessfulElapsed > 0 ? formatDuration(type.lastSuccessfulElapsed) : "none")")
            lines.append("  learnedTimeout: \(type.learnedTimeout > 0 ? formatDuration(type.learnedTimeout) : "none")")
            lines.append("  suggestedRetryTimeout: \(type.suggestedRetryTimeout > 0 ? formatDuration(type.suggestedRetryTimeout) : "none")")
            lines.append("  timeoutCount: \(type.timeoutCount)")
            lines.append("  successCount: \(type.successCount)")
            lines.append("  totalElapsed: \(formatDuration(type.totalElapsedSeconds))")
            lines.append("  fetchElapsed: \(formatDuration(type.timingBreakdown.fetchElapsedSeconds))")
            lines.append("  processingElapsed: \(formatDuration(type.timingBreakdown.processingElapsedSeconds))")
            if type.timingBreakdown.processingAccountedElapsedSeconds > 0 {
                lines.append("  processingDeltaApplyElapsed: \(formatDuration(type.timingBreakdown.processingDeltaApplyElapsedSeconds))")
                lines.append("  processingRecordArchiveRebuildElapsed: \(formatDuration(type.timingBreakdown.processingRecordArchiveRebuildElapsedSeconds))")
                lines.append("  processingInitialRecordElapsed: \(formatDuration(type.timingBreakdown.processingInitialRecordElapsedSeconds))")
                lines.append("  processingRecordArchiveFinalizeElapsed: \(formatDuration(type.timingBreakdown.processingRecordArchiveFinalizeElapsedSeconds))")
                lines.append("  processingOtherElapsed: \(formatDuration(type.timingBreakdown.processingOtherElapsedSeconds))")
            }
            lines.append("  insertElapsed: \(formatDuration(type.timingBreakdown.insertElapsedSeconds))")
            lines.append("  finalizeElapsed: \(formatDuration(type.timingBreakdown.finalizeElapsedSeconds))")
            if type.timingBreakdown.finalizeAccountedElapsedSeconds > 0 {
                lines.append("  finalizeEventCountElapsed: \(formatDuration(type.timingBreakdown.finalizeEventCountElapsedSeconds))")
                lines.append("  finalizeTypeSummaryElapsed: \(formatDuration(type.timingBreakdown.finalizeTypeSummaryElapsedSeconds))")
                lines.append("  finalizeDailyAggregateElapsed: \(formatDuration(type.timingBreakdown.finalizeDailyAggregateElapsedSeconds))")
                if type.timingBreakdown.finalizeDailyAggregateAccountedElapsedSeconds > 0 {
                    lines.append("  finalizeDailyAggregateBucketLookupElapsed: \(formatDuration(type.timingBreakdown.finalizeDailyAggregateBucketLookupElapsedSeconds))")
                    lines.append("  finalizeDailyAggregateCopyElapsed: \(formatDuration(type.timingBreakdown.finalizeDailyAggregateCopyElapsedSeconds))")
                    lines.append("  finalizeDailyAggregateDeleteElapsed: \(formatDuration(type.timingBreakdown.finalizeDailyAggregateDeleteElapsedSeconds))")
                    lines.append("  finalizeDailyAggregateRebuildElapsed: \(formatDuration(type.timingBreakdown.finalizeDailyAggregateRebuildElapsedSeconds))")
                    lines.append("  finalizeDailyAggregateInsertElapsed: \(formatDuration(type.timingBreakdown.finalizeDailyAggregateInsertElapsedSeconds))")
                    lines.append("  finalizeDailyAggregateOtherElapsed: \(formatDuration(type.timingBreakdown.finalizeDailyAggregateOtherElapsedSeconds))")
                }
                lines.append("  finalizeRunUpdateElapsed: \(formatDuration(type.timingBreakdown.finalizeRunUpdateElapsedSeconds))")
                lines.append("  finalizeOtherElapsed: \(formatDuration(type.timingBreakdown.finalizeOtherElapsedSeconds))")
            }
            lines.append("  residualElapsed: \(formatDuration(timingResidual(totalElapsed: type.totalElapsedSeconds, breakdown: type.timingBreakdown)))")
            if type.blockElapsedSeconds > 0 {
                lines.append("  fetchProgressElapsed: \(formatDuration(type.blockElapsedSeconds))")
            }
            if type.blockSamplesPerSecond > 0 {
                lines.append("  fetchProgressRate: \(formatRate(type.blockSamplesPerSecond)) samples/s")
            }
            if mode == .full || type.isUnsupported {
                lines.append("  unsupported: \(type.isUnsupported ? "true" : "false")")
            }
            let auth = normalizedAuthorization(for: type)
            lines.append("  authorizationStatus: \(auth.status)")
            lines.append("  authorizationStatusSource: \(auth.source)")
            lines.append("  earliestDate: \(metricDateString(type.earliestDate, queryType: "earliest_sample", calls: type.apiCallDetails))")
            lines.append("  latestDate: \(metricDateString(type.latestDate, queryType: "latest_sample", calls: type.apiCallDetails))")
            lines.append("  yearlyCounts: \(yearlyCountsString(type.yearlyCounts))")
            if type.recordCount == 0 {
                let zeroReason = type.quality == "complete" ? "complete query with zero records" : "\(type.authorizationStatus) authorization/query state"
                lines.append("  zeroCountContext: \(zeroReason)")
            }
            if type.apiCallDetails.contains(where: { $0.status == .timeout }) {
                lines.append("  interpretation: timeout policy failure, not HealthKit denial")
                lines.append("  action: Retry with extended timeout")
            }

            lines.append("  apiCalls:")
            for queryType in ["record_import", "earliest_sample", "latest_sample"] {
                if let call = type.apiCallDetails.first(where: { $0.queryType == queryType }) {
                    lines.append("    \(queryType):")
                    lines.append("      status: \(call.statusDescription)")
                    lines.append("      elapsed: \(String(format: "%.2f", call.elapsedSeconds))s")
                    if mode == .full || call.status != .complete {
                        lines.append("      result: \(callResultLabel(call))")
                    }
                    if shouldPrintCallErrorFields(call) {
                        if let failureKind = call.failureKind, !failureKind.isEmpty, failureKind != "none" {
                            lines.append("      failureKind: \(failureKind)")
                        }
                        if let cancellationReason = call.cancellationReason, !cancellationReason.isEmpty, cancellationReason != "none" {
                            lines.append("      cancellationReason: \(cancellationReason)")
                        }
                        if let errorDomain = call.errorDomain, !errorDomain.isEmpty, errorDomain != "none" {
                            lines.append("      errorDomain: \(errorDomain)")
                        }
                        if let errorCode = call.errorCode, !errorCode.isEmpty, errorCode != "none" {
                            lines.append("      errorCode: \(errorCode)")
                        }
                        if let errorMessage = call.errorDescription, !errorMessage.isEmpty, errorMessage != "none" {
                            lines.append("      errorMessage: \(errorMessage)")
                        }
                    }
                } else {
                    lines.append("    \(queryType):")
                    lines.append("      status: unknown")
                    lines.append("      elapsed: 0.00s")
                    if mode == .full {
                        lines.append("      result: unknown")
                        lines.append("      failureKind: not_run")
                    }
                }
            }
        }

        lines.append("")
        lines.append("DEVICE/APP CONTEXT")
        lines.append("OS: \(UIDevice.current.systemVersion)")
        lines.append("App Version: \(Bundle.main.appVersion)")
        lines.append("Build Fingerprint: \(Bundle.main.buildFingerprint)")
        lines.append("Source Commit: \(Bundle.main.sourceCommit)")
        lines.append("Source Dirty: \(Bundle.main.sourceDirty)")
        lines.append("")
        lines.append("END HEALTHPROBE REPORT")

        return lines.joined(separator: "\n")
    }

    private func plainTextStatus(for status: SnapshotFetchProgress.TypeProgress.FetchStatus) -> String {
        switch status {
        case .pending: return "Pending"
        case .fetching: return "Fetching"
        case .complete: return "Complete"
        case .failed(let message): return message.isEmpty ? "Failed" : "Failed (\(message))"
        }
    }

    private func fetchProgressSummary(_ progress: SnapshotFetchProgress) -> String {
        if progress.failedCount > 0 {
            return "\(progress.completedCount)/\(progress.totalTypeCount) fetched - \(progress.failedCount) failed"
        }
        return "\(progress.completedCount)/\(progress.totalTypeCount) fetched"
    }

    private func colorForStatus(_ status: SnapshotFetchProgress.TypeProgress.FetchStatus) -> Color {
        switch status {
        case .pending: return .gray
        case .fetching: return .blue
        case .complete: return .healthyGreen
        case .failed(let reason):
            return reason == "Not authorized" ? .warningAmber : .criticalRed
        }
    }

    private func rowBackground(for status: SnapshotFetchProgress.TypeProgress.FetchStatus) -> Color {
        switch status {
        case .fetching:
            return .blue.opacity(0.1)
        default:
            return Color(.systemGray6)
        }
    }

    private func rowBorder(for status: SnapshotFetchProgress.TypeProgress.FetchStatus) -> Color {
        switch status {
        case .fetching:
            return .blue.opacity(0.3)
        default:
            return .clear
        }
    }

    private var shouldShowFetchProgress: Bool {
        switch viewModel.snapshotProgress {
        case .fetching, .complete, .incomplete, .requiresResolution:
            return viewModel.fetchProgress != nil
        case .idle, .processing:
            return false
        }
    }

    private var shouldShowFetchReport: Bool {
        switch viewModel.snapshotProgress {
        case .complete, .incomplete, .requiresResolution:
            return true
        case .idle, .fetching, .processing:
            return false
        }
    }

    private func fetchReportView(_ progress: SnapshotFetchProgress) -> some View {
        let isIncomplete = viewModel.snapshotProgress == .incomplete
        let requiresResolution = viewModel.snapshotProgress == .requiresResolution
        let failed = isIncomplete ? failedTypesList(in: progress) : []
        let noteItems  = isIncomplete ? remediationNoteItems(progress: progress) : []
        let remediationTitle = hasAuthorizationFailures(progress) ? "WHAT TO DO" : "NOTES"
        return VStack(alignment: .leading, spacing: 20) {
            reportSummarySection(progress)
            if !failed.isEmpty { reportIssuesSection(failed) }
            if requiresResolution {
                ambiguousDisappearanceSection()
                reportRemediationSection(ambiguousDisappearanceNotes(), title: "NOTES")
                reportRemediationSection(ambiguousDisappearanceActions(), title: "WHAT TO DO")
            }
            if !noteItems.isEmpty { reportRemediationSection(noteItems, title: remediationTitle) }
            if isIncomplete, hasTimedOutMetrics(progress) { reportDecisionOverviewSection() }
            if isIncomplete { snapshotResultActionsSection }
            if requiresResolution { ambiguousResolutionActionsSection }
        }
    }

    private func reportSummarySection(_ progress: SnapshotFetchProgress) -> some View {
        VStack(alignment: .leading, spacing: 6) {
            Text("SUMMARY")
                .font(.caption.weight(.semibold))
                .foregroundStyle(.secondary)

            let isComplete = viewModel.snapshotProgress == .complete
            let requiresResolution = viewModel.snapshotProgress == .requiresResolution
            HStack(spacing: 10) {
                Image(systemName: isComplete ? "checkmark.circle.fill" : "exclamationmark.triangle.fill")
                    .font(.title3)
                    .foregroundStyle(isComplete ? Color.healthyGreen : Color.warningAmber)
                VStack(alignment: .leading, spacing: 2) {
                    Text(isComplete ? "Snapshot created successfully" : (requiresResolution ? "Snapshot needs review" : "Partial snapshot"))
                        .font(.subheadline.weight(.semibold))
                    Text(isComplete ? "All monitored metrics loaded" : (requiresResolution ? "\(viewModel.ambiguousDisappearedMetrics.count) metric\(viewModel.ambiguousDisappearedMetrics.count == 1 ? "" : "s") need review" : "\(progress.failedCount) metric\(progress.failedCount == 1 ? "" : "s") failed"))
                        .font(.caption)
                        .foregroundStyle(.secondary)
                }
                Spacer()
            }
            .padding(.horizontal, 12)
            .padding(.vertical, 10)
            .background(isComplete ? Color.healthyGreen.opacity(0.08) : Color.warningAmber.opacity(0.08))
            .cornerRadius(8)

            VStack(spacing: 0) {
                ReportRow(label: "Types processed", value: "\(progress.types.count)/\(progress.totalTypeCount)")
                Rectangle().fill(Color(.separator)).frame(height: 0.5).padding(.leading, 16)
                ReportRow(label: "Successful", value: "\(progress.completedCount)", valueColor: .healthyGreen)
                if progress.failedCount > 0 {
                    Rectangle().fill(Color(.separator)).frame(height: 0.5).padding(.leading, 16)
                    ReportRow(label: "Failed", value: "\(progress.failedCount)", valueColor: .criticalRed)
                }
                Rectangle().fill(Color(.separator)).frame(height: 0.5).padding(.leading, 16)
                ReportRow(label: "Records fetched", value: "\(progress.totalRecords)")
                Rectangle().fill(Color(.separator)).frame(height: 0.5).padding(.leading, 16)
                ReportRow(
                    label: "Quality",
                    value: qualityLabel(progress: progress),
                    valueColor: qualityColor(progress: progress)
                )
                if let secs = viewModel.fetchDurationSeconds {
                    Rectangle().fill(Color(.separator)).frame(height: 0.5).padding(.leading, 16)
                    ReportRow(label: "Duration", value: formatDuration(secs))
                }
                Rectangle().fill(Color(.separator)).frame(height: 0.5).padding(.leading, 16)
                ReportRow(label: "Trigger", value: viewModel.completedSnapshotTriggerReason ?? "manual")
                Rectangle().fill(Color(.separator)).frame(height: 0.5).padding(.leading, 16)
                ReportActionRow(label: "Diagnostics", value: "View", systemImage: "doc.text.magnifyingglass") {
                    diagnosticReport = DiagnosticReport(text: buildDiagnosticText(progress, mode: .full))
                }
            }
            .background(Color(.systemGray6))
            .cornerRadius(10)
        }
    }

    private func ambiguousDisappearanceSection() -> some View {
        VStack(alignment: .leading, spacing: 6) {
            Text("ISSUES")
                .font(.caption.weight(.semibold))
                .foregroundStyle(.secondary)

            Text("\(viewModel.ambiguousDisappearedMetrics.count) metric\(viewModel.ambiguousDisappearedMetrics.count == 1 ? "" : "s") returned zero samples")
                .font(.subheadline.weight(.semibold))
                .foregroundStyle(Color.warningAmber)

            VStack(spacing: 4) {
                ForEach(viewModel.ambiguousDisappearedMetrics) { metric in
                    HStack(spacing: 8) {
                        Image(systemName: "questionmark.circle.fill")
                            .foregroundStyle(Color.warningAmber)
                            .font(.caption)
                            .frame(width: 14)
                        Text(metric.displayName)
                            .font(.subheadline.weight(.semibold))
                        Spacer()
                        Text("all values missing")
                            .font(.caption)
                            .foregroundStyle(.secondary)
                    }
                    .padding(.horizontal, 12)
                    .padding(.vertical, 10)
                    .background(Color(.systemGray6))
                    .cornerRadius(8)
                }
            }
        }
    }

    private func reportIssuesSection(_ failed: [SnapshotFetchProgress.TypeProgress]) -> some View {
        VStack(alignment: .leading, spacing: 6) {
            Text("ISSUES")
                .font(.caption.weight(.semibold))
                .foregroundStyle(.secondary)

            Text("\(failed.count) metric\(failed.count == 1 ? "" : "s") failed")
                .font(.subheadline.weight(.semibold))
                .foregroundStyle(Color.criticalRed)

            VStack(spacing: 4) {
                ForEach(failed) { type in
                    let reason = failureReason(type)
                    let isExpanded = expandedIssueIDs.contains(type.id)
                    VStack(alignment: .leading, spacing: 0) {
                        // Collapsed row (always visible)
                        Button {
                            withAnimation(.easeInOut(duration: 0.2)) {
                                if isExpanded {
                                    expandedIssueIDs.remove(type.id)
                                } else {
                                    expandedIssueIDs.insert(type.id)
                                }
                            }
                        } label: {
                            HStack(spacing: 8) {
                                Image(systemName: reason == "Not authorized"
                                      ? "exclamationmark.triangle.fill" : "xmark.circle.fill")
                                    .foregroundStyle(reason == "Not authorized"
                                                     ? Color.warningAmber : Color.criticalRed)
                                    .font(.caption)
                                    .frame(width: 14)
                                Text(type.displayName)
                                    .font(.subheadline.weight(.semibold))
                                Text("— \(reason)")
                                    .font(.subheadline)
                                    .foregroundStyle(.secondary)
                                Spacer()
                                Image(systemName: isExpanded ? "chevron.up" : "chevron.down")
                                    .font(.caption2.weight(.semibold))
                                    .foregroundStyle(.tertiary)
                            }
                            .padding(.horizontal, 12)
                            .padding(.vertical, 10)
                            .contentShape(Rectangle())
                        }
                        .buttonStyle(.plain)

                        // Expanded detail (on tap)
                        if isExpanded {
                            Divider().padding(.horizontal, 12)
                            VStack(alignment: .leading, spacing: 4) {
                                Text("Reason: \(reason)").font(.caption).foregroundStyle(.secondary)
                                Text("Impact: \(failureImpact(reason))").font(.caption).foregroundStyle(.secondary)
                                if reason == "Timeout" {
                                    Text("Configured: \(formatDuration(type.timeoutConfiguredSeconds)) \(type.timeoutMode)")
                                        .font(.caption).foregroundStyle(.secondary)
                                    if type.learnedTimeout > 0 {
                                        Text("Observed needed: ~\(formatDuration(type.learnedTimeout))")
                                            .font(.caption).foregroundStyle(.secondary)
                                    }
                                    Text("Suggested retry: \(type.suggestedRetryTimeout > 0 ? formatDuration(type.suggestedRetryTimeout) : formatDuration(LocalMetricTimeoutProfile.maximumTimeout))")
                                        .font(.caption).foregroundStyle(.secondary)
                                }
                            }
                            .padding(.horizontal, 12)
                            .padding(.vertical, 8)
                            .frame(maxWidth: .infinity, alignment: .leading)
                        }
                    }
                    .background(Color(.systemGray6))
                    .cornerRadius(8)
                }
            }
        }
    }

    private func reportDecisionOverviewSection() -> some View {
        reportRemediationSection(
            [
                "Retry the full snapshot with extended timeout.",
                "Save the partial snapshot only if this incomplete report is useful.",
                "Discard snapshot and start a new one.",
                "Disable this metric in Settings if it consistently times out."
            ],
            title: "WHAT YOU CAN DO"
        )
    }

    private func reportRemediationSection(_ items: [String], title: String = "WHAT TO DO") -> some View {
        let isNotes = title == "NOTES"
        return VStack(alignment: .leading, spacing: 6) {
            Text(title)
                .font(isNotes ? .caption : .caption.weight(.semibold))
                .foregroundStyle(isNotes ? Color(.tertiaryLabel) : .secondary)
            VStack(alignment: .leading, spacing: isNotes ? 4 : 8) {
                ForEach(Array(items.enumerated()), id: \.offset) { index, item in
                    HStack(alignment: .top, spacing: 8) {
                        if isNotes {
                            Text("·")
                                .font(.caption2)
                                .foregroundStyle(Color(.tertiaryLabel))
                                .frame(width: 12, alignment: .trailing)
                        } else {
                            Text("\(index + 1).")
                                .font(.caption.weight(.semibold))
                                .foregroundStyle(.secondary)
                                .frame(width: 16, alignment: .trailing)
                        }
                        Text(item)
                            .font(isNotes ? .caption2 : .caption)
                            .foregroundStyle(isNotes ? Color(.secondaryLabel) : .primary)
                            .fixedSize(horizontal: false, vertical: true)
                    }
                }
            }
            .padding(12)
            .frame(maxWidth: .infinity, alignment: .leading)
            .background(Color(.systemGray6))
            .cornerRadius(8)
        }
    }

    private func progressFeedView(_ progress: SnapshotFetchProgress) -> some View {
        VStack(spacing: 12) {
            HStack {
                Text(fetchProgressSummary(progress))
                    .font(.caption.weight(.semibold))
                    .foregroundStyle(.secondary)
                Spacer()
                Text("\(progress.totalRecords) records")
                    .font(.caption)
                    .foregroundStyle(.secondary)
            }

            ScrollView {
                VStack(spacing: 8) {
                    ForEach(progress.visibleTypes) { type in
                        HStack(spacing: 12) {
                            if case .fetching = type.status {
                                ProgressView()
                                    .scaleEffect(0.8, anchor: .center)
                                    .frame(width: 20)
                            } else {
                                Image(systemName: type.status.icon)
                                    .font(.subheadline.weight(.semibold))
                                    .frame(width: 20)
                                    .foregroundStyle(colorForStatus(type.status))
                            }

                            VStack(alignment: .leading, spacing: 2) {
                                Text(type.displayName)
                                    .font(.caption)
                                    .lineLimit(1)
                                if !type.blockProgress.isEmpty {
                                    Text(type.blockProgress)
                                        .font(.caption2)
                                        .foregroundStyle(.secondary)
                                        .lineLimit(2)
                                }
                            }
                            .frame(maxWidth: .infinity, alignment: .leading)

                            fetchMetricStatsColumn(type, progress: progress)
                        }
                        .padding(.horizontal, 10)
                        .padding(.vertical, 9)
                        .background(rowBackground(for: type.status))
                        .cornerRadius(6)
                        .overlay {
                            RoundedRectangle(cornerRadius: 6)
                                .stroke(rowBorder(for: type.status), lineWidth: 1)
                        }
                    }
                }
                .frame(maxWidth: .infinity)
            }
        }
        .frame(maxHeight: .infinity)
    }

    private var pendingReportView: some View {
        VStack(spacing: 12) {
            Image(systemName: "doc.text.magnifyingglass")
                .font(.system(size: 36))
                .foregroundStyle(.secondary)
            Text("Report will appear when the snapshot finishes.")
                .font(.caption)
                .foregroundStyle(.secondary)
                .multilineTextAlignment(.center)
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }

    private var snapshotSheetTabPicker: some View {
        HStack(spacing: 2) {
            ForEach(SnapshotSheetTab.allCases) { tab in
                let isSelected = snapshotSheetTab == tab
                let isDisabled = tab == .report && !shouldShowFetchReport

                Button {
                    snapshotSheetTab = tab
                } label: {
                    Text(tab.rawValue)
                        .font(.caption.weight(.semibold))
                        .frame(maxWidth: .infinity)
                        .padding(.vertical, 8)
                        .foregroundStyle(isDisabled ? .tertiary : (isSelected ? .primary : .secondary))
                        .background(isSelected ? Color(.systemBackground) : Color.clear)
                        .cornerRadius(6)
                }
                .buttonStyle(.plain)
                .disabled(isDisabled)
                .accessibilityLabel(tab.rawValue)
            }
        }
        .padding(2)
        .background(Color(.systemGray5))
        .cornerRadius(8)
    }

    private var sheetOperationHeader: some View {
        VStack(alignment: .center, spacing: 4) {
            HStack(spacing: 8) {
                Image(systemName: "camera.viewfinder")
                    .font(.system(size: 20, weight: .semibold))
                    .foregroundStyle(Color(.secondaryLabel))
                    .frame(width: 22)

                HStack(spacing: 4) {
                    Text("Health data snapshot").font(.headline)

                    if viewModel.snapshotProgress == .complete {
                        Image(systemName: "checkmark.circle.fill")
                            .font(.caption)
                            .foregroundStyle(Color.healthyGreen)
                    } else if viewModel.snapshotProgress == .incomplete || viewModel.snapshotProgress == .requiresResolution {
                        Image(systemName: "exclamationmark.circle.fill")
                            .font(.caption)
                            .foregroundStyle(Color.warningAmber)
                    }
                }
            }

            if let progress = viewModel.fetchProgress {
                Text("\(progress.totalTypeCount) metrics")
                    .font(.caption)
                    .foregroundStyle(.secondary)
            }
        }
        .frame(maxWidth: .infinity)
    }

    @ViewBuilder
    private func fetchMetricStatsColumn(
        _ type: SnapshotFetchProgress.TypeProgress,
        progress: SnapshotFetchProgress
    ) -> some View {
        if case .fetching = type.status {
            TimelineView(.periodic(from: progress.startedAt, by: 1)) { context in
                let elapsed = max(type.blockElapsedSeconds, progress.elapsedSeconds(at: context.date))
                let rate = displayedRate(for: type, elapsedSeconds: elapsed)
                fetchMetricStatsContent(type, elapsedSeconds: elapsed, samplesPerSecond: rate)
            }
        } else if type.recordCount > 0 || type.blockElapsedSeconds > 0 || type.blockSamplesPerSecond > 0 {
            fetchMetricStatsContent(
                type,
                elapsedSeconds: type.blockElapsedSeconds,
                samplesPerSecond: displayedRate(for: type, elapsedSeconds: type.blockElapsedSeconds)
            )
        } else if case .failed(let reason) = type.status, reason == "Not authorized" {
            Text("Unavailable")
                .font(.caption2)
                .foregroundStyle(Color.warningAmber)
                .lineLimit(1)
        }
    }

    @ViewBuilder
    private func fetchMetricStatsContent(
        _ type: SnapshotFetchProgress.TypeProgress,
        elapsedSeconds: TimeInterval,
        samplesPerSecond: Double
    ) -> some View {
        if type.recordCount > 0 || elapsedSeconds > 0 || samplesPerSecond > 0 {
            VStack(alignment: .trailing, spacing: 2) {
                if type.recordCount > 0 {
                    Text("\(type.recordCount) records")
                }
                if elapsedSeconds > 0 {
                    Text(formatDuration(elapsedSeconds))
                }
                if samplesPerSecond > 0 {
                    Text("\(formatRate(samplesPerSecond)) rec/s")
                }
            }
            .font(.caption2)
            .foregroundStyle(.secondary)
            .lineLimit(1)
            .frame(minWidth: 82, alignment: .trailing)
        }
    }

    private func displayedRate(
        for type: SnapshotFetchProgress.TypeProgress,
        elapsedSeconds: TimeInterval
    ) -> Double {
        guard type.recordCount > 0, elapsedSeconds > 0 else { return 0 }
        return Double(type.recordCount) / elapsedSeconds
    }

    private func formatRate(_ value: Double) -> String {
        if value >= 1_000 {
            return value.formatted(.number.precision(.fractionLength(0)).grouping(.automatic))
        }
        if value >= 100 {
            return value.formatted(.number.precision(.fractionLength(0)))
        }
        if value >= 10 {
            return value.formatted(.number.precision(.fractionLength(1)))
        }
        return value.formatted(.number.precision(.fractionLength(2)))
    }

    // MARK: - Progress Sheet

    private var progressSheet: some View {
        VStack(spacing: 16) {
            sheetOperationHeader
                .frame(maxWidth: .infinity, alignment: .center)

            if let progress = viewModel.fetchProgress, shouldShowFetchProgress {
                VStack(spacing: 12) {
                    snapshotSheetTabPicker

                    Group {
                        switch snapshotSheetTab {
                        case .progress:
                            progressFeedView(progress)
                        case .report:
                            if shouldShowFetchReport {
                                ScrollView {
                                    fetchReportView(progress)
                                }
                            } else {
                                pendingReportView
                            }
                        }
                    }
                    .frame(maxHeight: .infinity)
                }
                .frame(maxHeight: .infinity)
            }

            if viewModel.snapshotProgress == .idle, let errorMsg = viewModel.snapshotError, !errorMsg.isEmpty {
                VStack(spacing: 12) {
                    HStack {
                        Text("Error details")
                            .font(.caption.weight(.semibold))
                        Spacer()
                        Button {
                            UIPasteboard.general.string = errorMsg
                        } label: {
                            Image(systemName: "doc.on.doc")
                                .font(.caption)
                        }
                        .accessibilityLabel("Copy error details to clipboard")
                    }
                    .foregroundStyle(.secondary)

                    Text(errorMsg)
                        .font(.caption)
                        .textSelection(.enabled)
                        .frame(maxWidth: .infinity, alignment: .leading)
                        .padding(12)
                        .background(Color(.systemGray6))
                        .cornerRadius(8)
                }
            }
        }
        .padding(24)
        .sheet(item: $diagnosticReport) { report in
            DiagnosticReportSheet(reportText: report.text)
        }
        .presentationDetents(sheetDetents)
        .presentationDragIndicator(.visible)
        .interactiveDismissDisabled(viewModel.snapshotProgress == .fetching || viewModel.snapshotProgress == .requiresResolution || viewModel.isRequestingAuth)
        .onAppear {
            snapshotSheetTab = .progress
        }
        .onChange(of: viewModel.snapshotProgress) { _, newProgress in
            if newProgress == .incomplete || newProgress == .requiresResolution {
                snapshotSheetTab = .report
                persistCompletedDiagnosticReportIfNeeded()
            } else if newProgress == .fetching {
                snapshotSheetTab = .progress
            } else if newProgress == .complete {
                persistCompletedDiagnosticReportIfNeeded()
            }
        }
    }

    private func persistCompletedDiagnosticReportIfNeeded() {
        guard let progress = viewModel.fetchProgress,
              let snapshotID = viewModel.completedSnapshotID,
              !savedDiagnosticSnapshotIDs.contains(snapshotID) else {
            return
        }
        let text = buildDiagnosticText(progress, mode: .full)
        do {
            try DiagnosticReportStore.persist(
                text: text,
                snapshotID: snapshotID,
                observationID: viewModel.completedArchiveObservationID,
                operationID: viewModel.operationID
            )
            savedDiagnosticSnapshotIDs.insert(snapshotID)
        } catch {
            print("Failed to persist diagnostic report: \(error.localizedDescription)")
        }
    }

    @ViewBuilder
    private var snapshotResultActionsSection: some View {
        let hasAuthorizationIssue = hasAuthorizationFailures(viewModel.fetchProgress)
        let hasTimeoutIssue = hasTimedOutMetrics(viewModel.fetchProgress)

        HStack(spacing: 12) {
            if hasAuthorizationIssue && !viewModel.canRetryWithPermissions {
                Button {
                    if let url = URL(string: UIApplication.openSettingsURLString) {
                        UIApplication.shared.open(url)
                    }
                } label: {
                    Text("Open Settings").frame(maxWidth: .infinity)
                }
                .buttonStyle(.borderedProminent)
                .disabled(viewModel.isCreatingSnapshot || viewModel.isRequestingAuth)
                .accessibilityLabel("Open app settings to restore HealthKit read access")
            } else if viewModel.canRetryWithPermissions || hasTimeoutIssue {
                Button {
                    Task {
                        if viewModel.canRetryWithPermissions {
                            await viewModel.retryWithPermissions(
                                selectedTypeIDs: appSettings.selectedTypeIDs,
                                adaptiveTimeoutsEnabled: appSettings.adaptiveTimeoutsEnabled
                            )
                        } else {
                            await viewModel.retryFailedMetricsWithExtendedTimeout(
                                selectedTypeIDs: appSettings.selectedTypeIDs,
                                adaptiveTimeoutsEnabled: appSettings.adaptiveTimeoutsEnabled
                            )
                        }
                    }
                } label: {
                    Text("Retry").frame(maxWidth: .infinity)
                }
                .buttonStyle(.borderedProminent)
                .disabled(viewModel.isCreatingSnapshot || viewModel.isRequestingAuth)
            }

            Button {
                Task { await viewModel.savePartialSnapshot() }
            } label: {
                Text("Save Partial").frame(maxWidth: .infinity)
            }
            .buttonStyle(.bordered)
            .disabled(viewModel.isCreatingSnapshot)
            .accessibilityLabel("Save partial snapshot")

            Button {
                Task { await viewModel.discardSnapshot() }
            } label: {
                Text("Discard").foregroundStyle(Color.criticalRed).frame(maxWidth: .infinity)
            }
            .buttonStyle(.bordered)
            .disabled(viewModel.isCreatingSnapshot)
            .accessibilityLabel("Discard snapshot")
        }
    }

    private var ambiguousResolutionActionsSection: some View {
        VStack(spacing: 10) {
            Button {
                Task { await viewModel.treatAmbiguousMetricsAsUnauthorized() }
            } label: {
                Label("Treat as Unauthorized", systemImage: "lock.slash")
                    .frame(maxWidth: .infinity)
            }
            .buttonStyle(.borderedProminent)
            .disabled(viewModel.isCreatingSnapshot)
            .accessibilityLabel("Treat missing metrics as unauthorized")

            Button {
                Task { await viewModel.treatAmbiguousMetricsAsDeleted() }
            } label: {
                Label("Treat as Missing", systemImage: "minus.circle")
                    .frame(maxWidth: .infinity)
            }
            .buttonStyle(.bordered)
            .disabled(viewModel.isCreatingSnapshot)
            .accessibilityLabel("Treat missing metrics as absent from the current Health store")

            Button {
                Task { await viewModel.discardSnapshot() }
            } label: {
                Label("Cancel Save", systemImage: "xmark")
                    .foregroundStyle(Color.criticalRed)
                    .frame(maxWidth: .infinity)
            }
            .buttonStyle(.bordered)
            .disabled(viewModel.isCreatingSnapshot)
            .accessibilityLabel("Cancel saving this snapshot")
        }
    }

    private var sheetDetents: Set<PresentationDetent> {
        [.large]
    }

    // MARK: - Sections

    private var statusSection: some View {
        Section("Status") {
            if let latestArchiveObservation {
                InfoRow(label: "Last Snapshot") {
                    Text(latestArchiveObservation.observedAt, style: .relative)
                        .foregroundStyle(.secondary)
                }
                InfoRow(label: "Records") {
                    Text("\(latestArchiveObservation.visibleRecordCount)")
                        .foregroundStyle(.secondary)
                }
                InfoRow(label: "Types") {
                    Text("\(latestArchiveObservation.trackedTypeCount)")
                        .foregroundStyle(.secondary)
                }
            } else if viewModel.isLoadingArchiveStatus {
                Label("Loading archive observations", systemImage: "clock.arrow.circlepath")
                    .foregroundStyle(.secondary)
            } else {
                Label("No archive observations yet", systemImage: "camera.viewfinder")
                    .foregroundStyle(.secondary)
            }

            if latestArchiveObservation != nil {
                InfoRow(label: "Device") {
                    Text(currentDeviceDisplayName)
                        .foregroundStyle(.secondary)
                }
            }

            InfoRow(label: "Monitored Types") {
                Text("\(appSettings.selectedTypeIDs.count)")
                    .foregroundStyle(.secondary)
            }

            if let archiveObservation = latestArchiveObservation {
                InfoRow(label: "Observation ID") {
                    Text("\(archiveObservation.observationID)")
                        .foregroundStyle(.secondary)
                }
            }

            if let archiveCacheError = viewModel.archiveCacheError {
                Label("Archive cache error", systemImage: "exclamationmark.triangle")
                    .font(.caption)
                    .foregroundStyle(Color.criticalRed)
                Text(archiveCacheError)
                    .font(.caption2)
                    .foregroundStyle(.secondary)
                    .lineLimit(2)
            } else if let archiveHealthStatus = viewModel.archiveHealthStatus {
                InfoRow(label: "Archive Cache") {
                    Text(archiveHealthStatus.lastIntegrityStatus == "ok" ? "OK" : archiveHealthStatus.lastIntegrityStatus)
                        .foregroundStyle(archiveHealthStatus.lastIntegrityStatus == "ok" ? Color.healthyGreen : Color.warningAmber)
                }
                InfoRow(label: "Cache Schema") {
                    Text("\(archiveHealthStatus.cacheSchemaVersion)")
                        .foregroundStyle(.secondary)
                }
            } else if !viewModel.isLoadingArchiveStatus {
                Label("Archive cache not built", systemImage: "externaldrive.badge.questionmark")
                    .font(.caption)
                    .foregroundStyle(.secondary)
            }

            if let archiveDelta = latestArchiveChangeCount {
                InfoRow(label: "Changes vs Previous") {
                    Text(archiveDelta == 0 ? "None" : "\(archiveDelta) records")
                        .foregroundStyle(archiveDelta == 0 ? Color.healthyGreen : Color.warningAmber)
                }
            }
        }
    }

    private var actionsSection: some View {
        Section("Actions") {
            Button {
                Task { await viewModel.requestAuthorization() }
            } label: {
                HStack {
                    Label("Request Health Access", systemImage: "heart.text.square")
                    Spacer()
                    if viewModel.isRequestingAuth { ProgressView() }
                }
            }
            .disabled(viewModel.isRequestingAuth)
            .accessibilityLabel("Request HealthKit read authorization")

            Button {
                Task {
                    await viewModel.createSnapshot(
                        selectedTypeIDs: appSettings.selectedTypeIDs,
                        adaptiveTimeoutsEnabled: appSettings.adaptiveTimeoutsEnabled
                    )
                }
            } label: {
                Label("Create Snapshot", systemImage: "camera.viewfinder")
            }
            .disabled(viewModel.isCreatingSnapshot)
            .accessibilityLabel("Create a new data snapshot")
        }
    }
}

private enum SnapshotSheetTab: String, CaseIterable, Identifiable {
    case progress = "Progress"
    case report = "Result"

    var id: Self { self }
}

private struct DiagnosticReport: Identifiable {
    let id = UUID()
    let text: String
}

// Avoids LabeledContent's TableRowContent ambiguity in List/Section contexts.
private struct InfoRow<Content: View>: View {
    let label: String
    @ViewBuilder let content: () -> Content

    var body: some View {
        HStack {
            Text(label)
            Spacer()
            content()
        }
    }
}

private struct ReportActionRow: View {
    let label: String
    let value: String
    let systemImage: String
    let action: () -> Void

    var body: some View {
        Button(action: action) {
            HStack(spacing: 10) {
                Label(label, systemImage: systemImage)
                    .font(.subheadline)
                    .labelStyle(.titleAndIcon)
                Spacer()
                HStack(spacing: 6) {
                    Text(value)
                        .font(.subheadline.weight(.semibold))
                        .foregroundStyle(.secondary)
                    Image(systemName: "chevron.right")
                        .font(.caption2.weight(.semibold))
                        .foregroundStyle(.tertiary)
                }
            }
            .padding(.horizontal, 12)
            .padding(.vertical, 10)
            .contentShape(Rectangle())
        }
        .buttonStyle(.plain)
        .accessibilityLabel("\(label), \(value)")
    }
}

private struct ReportRow: View {
    let label: String
    let value: String
    var valueColor: Color = .primary

    var body: some View {
        HStack {
            Text(label)
                .font(.subheadline)
            Spacer()
            Text(value)
                .font(.subheadline.weight(.semibold))
                .foregroundStyle(valueColor)
        }
        .padding(.horizontal, 12)
        .padding(.vertical, 10)
    }
}

extension Bundle {
    var appVersion: String {
        guard let version = infoDictionary?["CFBundleShortVersionString"] as? String else {
            return "unknown"
        }
        guard let build = infoDictionary?["CFBundleVersion"] as? String else {
            return version
        }
        return "\(version)(\(build))"
    }

    var sourceCommit: String {
        let value = infoDictionary?["HPSourceCommit"] as? String
        return value?.isEmpty == false ? value! : "unknown"
    }

    var sourceDirty: String {
        let value = infoDictionary?["HPSourceDirty"] as? String
        return value?.isEmpty == false ? value! : "unknown"
    }

    var buildFingerprint: String {
        guard let url = executableURL,
              let values = try? url.resourceValues(forKeys: [.contentModificationDateKey, .fileSizeKey]) else {
            return "unknown"
        }
        let modified = values.contentModificationDate?.timeIntervalSince1970 ?? 0
        let size = values.fileSize ?? 0
        return "\(appVersion)-\(Int(modified))-\(size)"
    }
}

#Preview {
    DashboardView()
        .environment(AppSettings())
}