import Foundation
import HealthKit
import SwiftData
import UIKit
import os.log

private let logger = Logger(subsystem: "ro.xdev.healthprobe", category: "HealthKitService")

enum TypeCategory: String, CaseIterable {
    case activity    = "Activity"
    case heart       = "Heart"
    case respiratory = "Respiratory"
    case sleep       = "Sleep"
    case hearing     = "Hearing"
    case body        = "Body"
}

struct MonitoredType: Identifiable, @unchecked Sendable {
    let id: String
    let displayName: String
    let category: TypeCategory
    let isEnabledByDefault: Bool
    let objectType: HKObjectType?   // nil = unsupported on this OS/device
}

extension Array where Element == MonitoredType {
    func sortedByFetchDisplayNameDescending() -> [MonitoredType] {
        sorted {
            let displayNameOrder = $0.displayName.localizedCaseInsensitiveCompare($1.displayName)
            if displayNameOrder == .orderedSame {
                return $0.id > $1.id
            }
            return displayNameOrder == .orderedDescending
        }
    }
}

final class HealthKitService {
    static let shared = HealthKitService()
    let store = HKHealthStore()

    static let allTypes: [MonitoredType] = buildAllTypes()

    static let defaultInitialTimeoutSeconds: TimeInterval = MetricTimeoutProfile.defaultInitialTimeout
    static let maximumTimeoutSeconds: TimeInterval = MetricTimeoutProfile.maximumTimeout
    // Prevents 3N simultaneous HK queries from exhausting resources at N=20 types.
    static let maxConcurrentTypeFetches = 6

    var isAvailable: Bool { HKHealthStore.isHealthDataAvailable() }

    var hasRequestedPermissionsBefore: Bool {
        get {
            UserDefaults.standard.bool(forKey: "healthkit.permissions.requested")
        }
        set {
            UserDefaults.standard.set(newValue, forKey: "healthkit.permissions.requested")
        }
    }

    // MARK: - Authorization

    func requestAuthorization() async throws {
        guard isAvailable else { return }
        let readTypes = Set(Self.allTypes.compactMap { $0.objectType })
        try await store.requestAuthorization(toShare: [], read: readTypes)
        hasRequestedPermissionsBefore = true
    }

    // MARK: - Snapshot creation

    @MainActor
    func createSnapshot(
        in context: ModelContext,
        selectedTypeIDs: Set<String>,
        adaptiveTimeoutsEnabled: Bool,
        triggerReason: String = "manual",
        retryOfSnapshotID: UUID? = nil,
        timeoutMultiplier: Double = 1,
        progress: SnapshotFetchProgress? = nil
    ) async throws -> HealthSnapshot {
        let active = Self.allTypes
            .filter { selectedTypeIDs.contains($0.id) }
            .sortedByFetchDisplayNameDescending()
        let deviceResolution = KeychainService.resolveDeviceID(swiftDataStoreIsEmpty: isStoreEmpty(context: context))

        let snapshot = HealthSnapshot(
            timestamp: Date(),
            osVersion: ProcessInfo.processInfo.operatingSystemVersionString,
            deviceName: UIDevice.current.name,
            deviceID: deviceResolution.id
        )
        snapshot.recoveredDeviceID = deviceResolution.isRecovered
        snapshot.triggerReason = triggerReason
        snapshot.retryOfSnapshotID = retryOfSnapshotID
        snapshot.yearlyCountTimezoneIdentifier = TimeZone.current.identifier
        // Fetch raw HealthKit data off the main actor, then assemble SwiftData models here
        // on the main actor to prevent data races on managed object context.
        let fetchResults = await fetchAllTypeCounts(
            for: active,
            context: context,
            adaptiveTimeoutsEnabled: adaptiveTimeoutsEnabled,
            timeoutMultiplier: timeoutMultiplier,
            progress: progress
        )
        let typeCounts: [TypeCount] = fetchResults.map { result in
            let tc = result.makeTypeCount()
            tc.snapshot = snapshot
            return tc
        }
        snapshot.typeCounts = typeCounts

        // Invariant assertions before save — debug asserts + release silent correction
        for tc in typeCounts {
            let isComplete = tc.quality == SnapshotQuality.complete
            assert(
                !isComplete || tc.count >= 0,
                "TypeCount with quality .complete must have count >= 0"
            )
            assert(
                isComplete || tc.count == -1,
                "TypeCount with quality != .complete must have count == -1"
            )
            if !isComplete && tc.count != -1 {
                logger.critical("TypeCount invariant violation: quality=\(tc.quality.rawValue) count=\(tc.count) type=\(tc.typeIdentifier)")
                tc.count = -1
            }
        }

        snapshot.snapshotQuality = deriveSnapshotQuality(from: typeCounts)

        configureSnapshotMetadata(
            snapshot,
            typeCounts: typeCounts,
            intendedTypeIDs: active.map { $0.id },
            context: context
        )

        if snapshot.snapshotQuality == .complete {
            try await persistSnapshot(snapshot, typeCounts: typeCounts, context: context)
        }

        return snapshot
    }

    @MainActor
    func savePartialSnapshot(_ snapshot: HealthSnapshot, in context: ModelContext) async throws -> HealthSnapshot {
        let typeCounts = snapshot.typeCounts ?? []
        guard snapshot.snapshotQuality != .complete else {
            return snapshot
        }

        configureSnapshotMetadata(
            snapshot,
            typeCounts: typeCounts,
            intendedTypeIDs: typeCounts.map(\.typeIdentifier),
            context: context
        )
        try await persistSnapshot(snapshot, typeCounts: typeCounts, context: context)
        return snapshot
    }

    // MARK: - Snapshot persistence

    private func persistSnapshot(
        _ snapshot: HealthSnapshot,
        typeCounts: [TypeCount],
        context: ModelContext
    ) async throws {
        context.insert(snapshot)
        for typeCount in typeCounts {
            context.insert(typeCount)
            for yearlyCount in typeCount.yearlyCounts ?? [] {
                context.insert(yearlyCount)
            }
            typeCount.snapshot = snapshot
        }
        snapshot.typeCounts = typeCounts

        try context.save()

        // Post-save pipeline: delta computation + anomaly detection
        try await runPostSavePipeline(snapshot: snapshot, typeCounts: typeCounts, context: context)
    }

    private func configureSnapshotMetadata(
        _ snapshot: HealthSnapshot,
        typeCounts: [TypeCount],
        intendedTypeIDs: [String],
        context: ModelContext
    ) {
        // Chain metadata — set BEFORE context.save()
        // localSequenceNumber is used here solely to find the latest local candidate during
        // snapshot creation. Once previousSnapshotID is set, all chain reconstruction must use
        // previousSnapshotID exclusively — never reconstruct a chain by walking localSequenceNumber.
        let previous = findPreviousSnapshot(deviceID: snapshot.deviceID, excluding: snapshot.id, context: context)
        if let previous {
            snapshot.previousSnapshotID = previous.id
            snapshot.localSequenceNumber = previous.localSequenceNumber + 1
            snapshot.isChainStart = false

            snapshot.monitoredTypeSetHash = HashService.typeSetHash(typeIDs: intendedTypeIDs)
            if snapshot.monitoredTypeSetHash != previous.monitoredTypeSetHash {
                snapshot.monitoredRegistryVersion = previous.monitoredRegistryVersion + 1
            } else {
                snapshot.monitoredRegistryVersion = previous.monitoredRegistryVersion
            }

            // Auto-detect post-restore: previous was fully unauthorized, current is complete
            if previous.snapshotQuality == SnapshotQuality.unauthorized && snapshot.snapshotQuality == SnapshotQuality.complete {
                snapshot.isPostRestore = true
                snapshot.isPostRestoreInferred = true
            }
        } else {
            snapshot.previousSnapshotID = nil
            snapshot.localSequenceNumber = 0
            snapshot.isChainStart = true
            snapshot.monitoredTypeSetHash = HashService.typeSetHash(typeIDs: intendedTypeIDs)
            snapshot.monitoredRegistryVersion = 0

            // Auto-detect post-restore on chain start with significant data
            let completeTypeCounts = typeCounts.filter { $0.quality == SnapshotQuality.complete }
            let completeCount = completeTypeCounts.reduce(0) { $0 + max($1.count, 0) }
            if completeCount > 1000 {
                snapshot.isPostRestore = true
                snapshot.isPostRestoreInferred = true
            }
        }

        // Device metadata — informational only, never used for chain linkage
        snapshot.hardwareModel = hardwareModel()
        snapshot.appBuildVersion = appBuildVersion()
    }

    // MARK: - Post-save pipeline

    private func runPostSavePipeline(
        snapshot: HealthSnapshot,
        typeCounts: [TypeCount],
        context: ModelContext
    ) async throws {
        guard let prevID = snapshot.previousSnapshotID else { return }

        let prevDescriptor = FetchDescriptor<HealthSnapshot>(
            predicate: #Predicate<HealthSnapshot> { $0.id == prevID }
        )
        guard let previous = try context.fetch(prevDescriptor).first else { return }

        guard let delta = try DeltaService.computeAndSave(current: snapshot, context: context) else { return }

        // Build type count maps for AnomalyDetector (never access relationship properties directly)
        let currentTypeCounts = Dictionary(uniqueKeysWithValues: typeCounts.map { ($0.typeIdentifier, $0) })
        let previousTypeCounts = Dictionary(
            uniqueKeysWithValues: (previous.typeCounts ?? []).map { ($0.typeIdentifier, $0) }
        )

        let detection = AnomalyDetector.detect(
            delta: delta,
            current: snapshot,
            previous: previous,
            currentTypeCounts: currentTypeCounts,
            previousTypeCounts: previousTypeCounts
        )

        for record in detection.records {
            context.insert(record)
        }
        if let consumedDeltaID = detection.consumedPostRestoreSuppressionDeltaID {
            previous.isPostRestoreSuppressedDeltaID = consumedDeltaID
        }

        if !detection.records.isEmpty || detection.consumedPostRestoreSuppressionDeltaID != nil {
            try context.save()
        }
    }

    // MARK: - Per-type fetch pipeline

    // Returns raw Sendable fetch results — no SwiftData model creation here.
    // All TypeCount/YearlyCount objects must be created on the main actor in createSnapshot.
    // Fetches sequentially to prevent race conditions and resource exhaustion.
    private func fetchAllTypeCounts(
        for active: [MonitoredType],
        context: ModelContext,
        adaptiveTimeoutsEnabled: Bool,
        timeoutMultiplier: Double,
        progress: SnapshotFetchProgress? = nil
    ) async -> [TypeCountFetchResult] {
        var results: [TypeCountFetchResult] = []

        for monitoredType in active {
            let profile = timeoutProfile(for: monitoredType, context: context)
            let timeout = timeoutForFetch(
                profile: profile,
                adaptiveTimeoutsEnabled: adaptiveTimeoutsEnabled,
                timeoutMultiplier: timeoutMultiplier
            )
            var result = await fetchTypeCountData(
                for: monitoredType,
                timeoutProfile: profile,
                timeoutSeconds: timeout,
                progress: progress
            )
            updateTimeoutProfile(profile, with: result, monitoredType: monitoredType)
            result.applyTimeoutProfile(profile)
            progress?.updateTimeoutProfile(from: profile, for: monitoredType.id)
            results.append(result)
        }
        return results
    }

    private func fetchTypeCountData(
        for monitoredType: MonitoredType,
        timeoutProfile: MetricTimeoutProfile,
        timeoutSeconds: TimeInterval,
        progress: SnapshotFetchProgress? = nil
    ) async -> TypeCountFetchResult {
        let started = Date()
        progress?.updateStatus(monitoredType.id, status: .fetching)

        // Unsupported type: HKObjectType factory returned nil for this identifier
        guard let objectType = monitoredType.objectType,
              let sampleType = objectType as? HKSampleType else {
            var result = TypeCountFetchResult(
                typeIdentifier: monitoredType.id,
                displayName: monitoredType.displayName,
                count: -1,
                contentHash: "",
                earliestDate: nil,
                latestDate: nil,
                quality: SnapshotQuality.failed,
                diagnosticQuality: HealthKitAPICallResult.Status.unsupported.rawValue,
                isUnsupported: true,
                authorizationStatus: "unavailable",
                apiCalls: Self.placeholderAPICalls(status: .unsupported, message: "HealthKit type is not available on this OS or device"),
                yearlyCounts: []
            )
            result.totalElapsedSeconds = Date().timeIntervalSince(started)
            result.timeoutConfiguredSeconds = timeoutSeconds
            result.applyTimeoutProfile(timeoutProfile)
            progress?.updateDetails(from: result)
            progress?.updateStatus(monitoredType.id, status: .failed("Unsupported"))
            return result
        }

        var result = await fetchTypeCountDataFromHK(monitoredType: monitoredType, sampleType: sampleType, timeoutSeconds: timeoutSeconds)
        result.totalElapsedSeconds = Date().timeIntervalSince(started)
        result.timeoutConfiguredSeconds = timeoutSeconds
        result.applyTimeoutProfile(timeoutProfile)
        progress?.updateDetails(from: result)

        if result.diagnosticQuality == SnapshotQuality.complete.rawValue {
            progress?.updateStatus(monitoredType.id, status: .complete, recordCount: max(result.count, 0))
        } else if result.apiCalls.contains(where: { $0.status == .timeout }) {
            progress?.updateStatus(monitoredType.id, status: .failed("Timeout"))
        } else if result.diagnosticQuality == SnapshotQuality.unauthorized.rawValue {
            progress?.updateStatus(monitoredType.id, status: .failed("Not authorized"))
        } else {
            progress?.updateStatus(monitoredType.id, status: .failed("Failed"))
        }

        return result
    }

    private func fetchTypeCountDataFromHK(monitoredType: MonitoredType, sampleType: HKSampleType, timeoutSeconds: TimeInterval) async -> TypeCountFetchResult {
        let deadline = Date().addingTimeInterval(timeoutSeconds)
        let distributionResult = await measureAPICall(
            queryType: "distribution",
            timeoutSeconds: max(0, deadline.timeIntervalSinceNow)
        ) {
            try await self.fetchDistribution(for: sampleType)
        } resultDescription: { distribution in
            "\(distribution.totalCount) samples"
        }

        guard distributionResult.apiCall.status == .complete, let distribution = distributionResult.value else {
            let status = distributionResult.apiCall.status
            let quality = diagnosticQuality(for: status)
            return TypeCountFetchResult(
                typeIdentifier: monitoredType.id,
                displayName: monitoredType.displayName,
                count: -1,
                contentHash: "",
                earliestDate: nil,
                latestDate: nil,
                quality: snapshotQuality(for: status),
                diagnosticQuality: quality,
                isUnsupported: false,
                authorizationStatus: authorizationStatus(from: [distributionResult.apiCall]),
                apiCalls: [
                    distributionResult.apiCall,
                    Self.placeholderAPICall(queryType: "earliest_sample", status: .unknown, message: "query not run"),
                    Self.placeholderAPICall(queryType: "latest_sample", status: .unknown, message: "query not run")
                ],
                yearlyCounts: []
            )
        }

        // Both date queries share the same 15s budget with the distribution query.
        let remainingSeconds = max(0, deadline.timeIntervalSinceNow)
        async let earliestTask = measureAPICall(
            queryType: "earliest_sample",
            timeoutSeconds: remainingSeconds
        ) {
            try await self.fetchEarliestDate(for: sampleType)
        } resultDescription: { date in
            Self.iso8601String(for: date)
        }
        async let latestTask = measureAPICall(
            queryType: "latest_sample",
            timeoutSeconds: remainingSeconds
        ) {
            try await self.fetchLatestDate(for: sampleType)
        } resultDescription: { date in
            Self.iso8601String(for: date)
        }
        let earliestResult = await earliestTask
        let latestResult = await latestTask
        let apiCalls = [distributionResult.apiCall, earliestResult.apiCall, latestResult.apiCall]

        guard earliestResult.apiCall.status == .complete, latestResult.apiCall.status == .complete else {
            let status = firstImpairedStatus(in: apiCalls)
            let quality = diagnosticQuality(for: status)
            return TypeCountFetchResult(
                typeIdentifier: monitoredType.id,
                displayName: monitoredType.displayName,
                count: -1,
                contentHash: "",
                earliestDate: earliestResult.value ?? nil,
                latestDate: latestResult.value ?? nil,
                quality: snapshotQuality(for: status),
                diagnosticQuality: quality,
                isUnsupported: false,
                authorizationStatus: authorizationStatus(from: apiCalls),
                apiCalls: apiCalls,
                yearlyCounts: []
            )
        }

        let earliest = earliestResult.value ?? nil
        let latest = latestResult.value ?? nil
        let contentHash = HashService.typeHash(
            typeIdentifier: monitoredType.id,
            totalCount: distribution.totalCount,
            earliestDate: earliest,
            latestDate: latest
        )

        // YearlyCount uses Calendar.current; year attribution is local-time based.
        let isApprox = DistributionCaptureConfiguration.bucketComponent != .day
        var yearMap: [Int: Int] = [:]
        for bin in distribution.bins {
            let year = Calendar.current.component(.year, from: bin.start)
            yearMap[year, default: 0] += bin.count
        }
        let yearlyCounts = yearMap.map { year, yearCount in
            TypeCountFetchResult.YearlyCountData(
                year: year,
                count: yearCount,
                typeIdentifier: monitoredType.id,
                isApproximate: isApprox
            )
        }

        return TypeCountFetchResult(
            typeIdentifier: monitoredType.id,
            displayName: monitoredType.displayName,
            count: distribution.totalCount,
            contentHash: contentHash,
            earliestDate: earliest,
            latestDate: latest,
            quality: SnapshotQuality.complete,
            diagnosticQuality: HealthKitAPICallResult.Status.complete.rawValue,
            isUnsupported: false,
            authorizationStatus: authorizationStatus(from: apiCalls),
            apiCalls: apiCalls,
            yearlyCounts: yearlyCounts
        )
    }

    // MARK: - HealthKit queries

    private func fetchDistribution(for sampleType: HKSampleType) async throws -> SampleDistribution {
        try await withCheckedThrowingContinuation { continuation in
            let query = HKSampleQuery(
                sampleType: sampleType,
                predicate: nil,
                limit: HKObjectQueryNoLimit,
                sortDescriptors: nil
            ) { _, samples, error in
                if let error {
                    continuation.resume(throwing: error)
                    return
                }
                let samples = samples ?? []
                let calendar = Calendar.current
                var countsByDay: [Date: Int] = [:]
                for sample in samples {
                    guard let day = calendar.dateInterval(
                        of: DistributionCaptureConfiguration.bucketComponent,
                        for: sample.startDate
                    ) else { continue }
                    countsByDay[day.start, default: 0] += 1
                }
                let bins = countsByDay.map { dayStart, count in
                    SampleDistribution.Bin(
                        start: dayStart,
                        end: calendar.date(
                            byAdding: DistributionCaptureConfiguration.bucketComponent,
                            value: DistributionCaptureConfiguration.bucketStep,
                            to: dayStart
                        ) ?? dayStart,
                        count: count
                    )
                }.sorted { $0.start < $1.start }
                continuation.resume(returning: SampleDistribution(totalCount: samples.count, bins: bins))
            }
            store.execute(query)
        }
    }

    private func fetchEarliestDate(for sampleType: HKSampleType) async throws -> Date? {
        try await withCheckedThrowingContinuation { continuation in
            let query = HKSampleQuery(
                sampleType: sampleType,
                predicate: nil,
                limit: 1,
                sortDescriptors: [NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: true)]
            ) { _, samples, error in
                if let error { continuation.resume(throwing: error); return }
                continuation.resume(returning: samples?.first?.startDate)
            }
            store.execute(query)
        }
    }

    private func fetchLatestDate(for sampleType: HKSampleType) async throws -> Date? {
        try await withCheckedThrowingContinuation { continuation in
            let query = HKSampleQuery(
                sampleType: sampleType,
                predicate: nil,
                limit: 1,
                sortDescriptors: [NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: false)]
            ) { _, samples, error in
                if let error { continuation.resume(throwing: error); return }
                continuation.resume(returning: samples?.first?.startDate)
            }
            store.execute(query)
        }
    }

    // MARK: - Timeout profiles

    private func timeoutProfile(for monitoredType: MonitoredType, context: ModelContext) -> MetricTimeoutProfile {
        let identifier = monitoredType.id
        let descriptor = FetchDescriptor<MetricTimeoutProfile>(
            predicate: #Predicate<MetricTimeoutProfile> { $0.metricIdentifier == identifier }
        )
        if let existing = try? context.fetch(descriptor).first {
            existing.displayName = monitoredType.displayName
            return existing
        }

        let profile = MetricTimeoutProfile(metricIdentifier: monitoredType.id, displayName: monitoredType.displayName)
        context.insert(profile)
        return profile
    }

    private func timeoutForFetch(
        profile: MetricTimeoutProfile,
        adaptiveTimeoutsEnabled: Bool,
        timeoutMultiplier: Double
    ) -> TimeInterval {
        let base = adaptiveTimeoutsEnabled ? profile.effectiveTimeout : Self.defaultInitialTimeoutSeconds
        return min(max(base * timeoutMultiplier, Self.defaultInitialTimeoutSeconds), Self.maximumTimeoutSeconds)
    }

    private func updateTimeoutProfile(
        _ profile: MetricTimeoutProfile,
        with result: TypeCountFetchResult,
        monitoredType: MonitoredType
    ) {
        if result.quality == .complete {
            profile.recordSuccess(elapsed: result.totalElapsedSeconds, displayName: monitoredType.displayName)
        } else if result.apiCalls.contains(where: { $0.status == .timeout }) {
            profile.recordTimeout(elapsed: result.totalElapsedSeconds, displayName: monitoredType.displayName)
        } else {
            profile.displayName = monitoredType.displayName
        }
    }

    // MARK: - Query diagnostics

    private struct APICallMeasurement<Value: Sendable>: Sendable {
        let value: Value?
        let apiCall: HealthKitAPICallResult
    }

    private func measureAPICall<Value: Sendable>(
        queryType: String,
        timeoutSeconds: TimeInterval,
        operation: @escaping @Sendable () async throws -> Value,
        resultDescription: @escaping @Sendable (Value) -> String
    ) async -> APICallMeasurement<Value> {
        let started = Date()
        do {
            guard timeoutSeconds > 0 else { throw CancellationError() }
            let value = try await withTimeout(seconds: timeoutSeconds, operation: operation)
            return APICallMeasurement(
                value: value,
                apiCall: HealthKitAPICallResult(
                    queryType: queryType,
                    status: .complete,
                    elapsedSeconds: Date().timeIntervalSince(started),
                    resultValue: resultDescription(value)
                )
            )
        } catch {
            let failure = Self.apiFailure(for: error)
            let nsError = error as NSError
            return APICallMeasurement(
                value: nil,
                apiCall: HealthKitAPICallResult(
                    queryType: queryType,
                    status: failure.status,
                    elapsedSeconds: Date().timeIntervalSince(started),
                    resultValue: nil,
                    errorCode: failure.errorCode ?? "\(nsError.code)",
                    errorDomain: failure.errorDomain ?? nsError.domain,
                    errorDescription: failure.errorDescription ?? nsError.localizedDescription,
                    failureKind: failure.failureKind,
                    cancellationReason: failure.cancellationReason
                )
            )
        }
    }

    private static func apiFailure(for error: Error) -> (
        status: HealthKitAPICallResult.Status,
        failureKind: String,
        errorDomain: String?,
        errorCode: String?,
        errorDescription: String?,
        cancellationReason: String?
    ) {
        if error is CancellationError {
            return (
                .timeout,
                "internal cancellation",
                "HealthProbe",
                "perTypeTimeout",
                "HealthProbe cancelled this query after the per-type timeout",
                "per-type timeout reached"
            )
        }

        let nsError = error as NSError
        if nsError.domain == NSURLErrorDomain && nsError.code == NSURLErrorTimedOut {
            return (.timeout, "timeout", nil, nil, nil, nil)
        }

        if (error as? HKError)?.code == .errorAuthorizationDenied {
            return (.unauthorized, "HealthKit error", nil, nil, nil, nil)
        }

        return (.failed, "HealthKit error", nil, nil, nil, nil)
    }

    private func authorizationStatus(from apiCalls: [HealthKitAPICallResult]) -> String {
        if apiCalls.contains(where: { $0.status == .complete }) {
            return "granted"
        }
        if apiCalls.contains(where: { $0.status == .unauthorized }) {
            return "denied"
        }
        return HealthKitAPICallResult.Status.unknown.rawValue
    }

    private func snapshotQuality(for status: HealthKitAPICallResult.Status) -> SnapshotQuality {
        status == .unauthorized ? .unauthorized : .failed
    }

    private func diagnosticQuality(for status: HealthKitAPICallResult.Status) -> String {
        switch status {
        case .complete:
            return HealthKitAPICallResult.Status.complete.rawValue
        case .unauthorized:
            return HealthKitAPICallResult.Status.unauthorized.rawValue
        case .unsupported:
            return HealthKitAPICallResult.Status.unsupported.rawValue
        case .timeout:
            return SnapshotQuality.failed.rawValue
        case .failed:
            return HealthKitAPICallResult.Status.failed.rawValue
        case .unknown:
            return HealthKitAPICallResult.Status.unknown.rawValue
        }
    }

    private func firstImpairedStatus(in apiCalls: [HealthKitAPICallResult]) -> HealthKitAPICallResult.Status {
        let statuses = apiCalls.map(\.status)
        if statuses.contains(.unauthorized) { return .unauthorized }
        if statuses.contains(.timeout) { return .timeout }
        if statuses.contains(.unsupported) { return .unsupported }
        if statuses.contains(.failed) { return .failed }
        return .unknown
    }

    private static func placeholderAPICalls(status: HealthKitAPICallResult.Status, message: String) -> [HealthKitAPICallResult] {
        [
            placeholderAPICall(queryType: "distribution", status: status, message: message),
            placeholderAPICall(queryType: "earliest_sample", status: status, message: message),
            placeholderAPICall(queryType: "latest_sample", status: status, message: message)
        ]
    }

    private static func placeholderAPICall(
        queryType: String,
        status: HealthKitAPICallResult.Status,
        message: String
    ) -> HealthKitAPICallResult {
        HealthKitAPICallResult(
            queryType: queryType,
            status: status,
            elapsedSeconds: 0,
            resultValue: nil,
            errorCode: "none",
            errorDomain: "none",
            errorDescription: message,
            failureKind: status == .unknown ? "not run" : status.rawValue,
            cancellationReason: "none"
        )
    }

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

    // MARK: - Quality aggregation

    func deriveSnapshotQuality(from typeCounts: [TypeCount]) -> SnapshotQuality {
        guard !typeCounts.isEmpty else { return .failed }
        if typeCounts.contains(where: { $0.quality == .loading }) { return .loading }
        let allUnauthorized = typeCounts.allSatisfy { $0.quality == .unauthorized }
        if allUnauthorized { return .unauthorized }
        let anyImpaired = typeCounts.contains { $0.quality == .failed || $0.quality == .unauthorized }
        if anyImpaired { return .partial }
        return .complete
    }

    // MARK: - Chain helpers

    private func findPreviousSnapshot(deviceID: String, excluding id: UUID, context: ModelContext) -> HealthSnapshot? {
        let descriptor = FetchDescriptor<HealthSnapshot>(
            predicate: #Predicate<HealthSnapshot> { $0.deviceID == deviceID && $0.id != id },
            sortBy: [SortDescriptor(\.localSequenceNumber, order: .reverse)]
        )
        return try? context.fetch(descriptor).first
    }

    private func isStoreEmpty(context: ModelContext) -> Bool {
        let descriptor = FetchDescriptor<HealthSnapshot>()
        return (try? context.fetch(descriptor).isEmpty) ?? true
    }

    // MARK: - Device metadata

    private func hardwareModel() -> String {
        var size = 0
        sysctlbyname("hw.machine", nil, &size, nil, 0)
        var machine = [CChar](repeating: 0, count: size)
        sysctlbyname("hw.machine", &machine, &size, nil, 0)
        return String(cString: machine)
    }

    private func appBuildVersion() -> String {
        let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
        let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? ""
        return "\(version) (\(build))"
    }

    // MARK: - Timeout utility

    private func withTimeout<T: Sendable>(seconds: TimeInterval, operation: @escaping @Sendable () async throws -> T) async throws -> T {
        try await withThrowingTaskGroup(of: T.self) { group in
            group.addTask { try await operation() }
            group.addTask {
                try await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
                throw CancellationError()
            }
            let result = try await group.next()!
            group.cancelAll()
            return result
        }
    }

    // MARK: - Type registry

    private static func buildAllTypes() -> [MonitoredType] {
        var result: [MonitoredType] = []

        func addQty(_ id: HKQuantityTypeIdentifier, _ name: String, _ cat: TypeCategory, on: Bool) {
            let t = HKObjectType.quantityType(forIdentifier: id)
            result.append(MonitoredType(id: t?.identifier ?? id.rawValue, displayName: name, category: cat, isEnabledByDefault: on, objectType: t))
        }

        func addCat(_ id: HKCategoryTypeIdentifier, _ name: String, _ cat: TypeCategory, on: Bool) {
            let t = HKObjectType.categoryType(forIdentifier: id)
            result.append(MonitoredType(id: t?.identifier ?? id.rawValue, displayName: name, category: cat, isEnabledByDefault: on, objectType: t))
        }

        let workout = HKObjectType.workoutType()
        result.append(MonitoredType(id: workout.identifier, displayName: "Workouts", category: .activity, isEnabledByDefault: true, objectType: workout))

        addQty(.stepCount,              "Steps",                          .activity,    on: true)
        addQty(.distanceWalkingRunning, "Walking + Running Distance",     .activity,    on: true)
        addQty(.activeEnergyBurned,     "Active Energy",                  .activity,    on: true)
        addQty(.appleExerciseTime,      "Exercise Minutes",               .activity,    on: true)
        addCat(.appleStandHour,         "Stand Hours",                    .activity,    on: true)

        addQty(.heartRate,              "Heart Rate",                     .heart,       on: true)
        addQty(.restingHeartRate,       "Resting Heart Rate",             .heart,       on: true)
        addCat(.highHeartRateEvent,     "High Heart Rate Notifications",  .heart,       on: true)

        addQty(.respiratoryRate,        "Respiratory Rate",               .respiratory, on: true)

        addCat(.sleepAnalysis,          "Sleep",                          .sleep,       on: true)

        addQty(.environmentalAudioExposure, "Environmental Sound Levels", .hearing,     on: false)
        addQty(.headphoneAudioExposure,     "Headphone Audio Levels",     .hearing,     on: false)

        addQty(.bodyMass, "Body Mass", .body, on: false)
        addQty(.vo2Max,   "VO2 Max",   .body, on: false)

        return result
    }
}

private struct SampleDistribution: Sendable {
    struct Bin: Sendable {
        let start: Date
        let end: Date
        let count: Int
    }
    let totalCount: Int
    let bins: [Bin]
}

private struct TypeCountFetchResult: Sendable {
    struct YearlyCountData: Sendable {
        let year: Int
        let count: Int
        let typeIdentifier: String
        let isApproximate: Bool
    }

    let typeIdentifier: String
    let displayName: String
    let count: Int
    let contentHash: String
    let earliestDate: Date?
    let latestDate: Date?
    let quality: SnapshotQuality
    let diagnosticQuality: String
    let isUnsupported: Bool
    let authorizationStatus: String
    let apiCalls: [HealthKitAPICallResult]
    let yearlyCounts: [YearlyCountData]
    var timeoutConfiguredSeconds: TimeInterval = 0
    var totalElapsedSeconds: TimeInterval = 0
    var timeoutMode: String = "default"
    var lastSuccessfulElapsed: TimeInterval = 0
    var learnedTimeout: TimeInterval = 0
    var suggestedRetryTimeout: TimeInterval = 0
    var timeoutCount: Int = 0
    var successCount: Int = 0

    mutating func applyTimeoutProfile(_ profile: MetricTimeoutProfile) {
        timeoutMode = profile.timeoutMode
        lastSuccessfulElapsed = profile.lastSuccessfulElapsed
        learnedTimeout = profile.effectiveTimeout
        suggestedRetryTimeout = profile.suggestedRetryTimeout
        timeoutCount = profile.timeoutCount
        successCount = profile.successCount
    }

    @MainActor
    func makeTypeCount() -> TypeCount {
        let typeCount = TypeCount(
            typeIdentifier: typeIdentifier,
            displayName: displayName,
            count: count,
            quality: quality
        )
        typeCount.contentHash = contentHash
        typeCount.earliestDate = earliestDate
        typeCount.latestDate = latestDate
        typeCount.isUnsupported = isUnsupported

        for yearlyCountData in yearlyCounts {
            let yearlyCount = YearlyCount(
                year: yearlyCountData.year,
                count: yearlyCountData.count,
                typeIdentifier: yearlyCountData.typeIdentifier,
                isApproximate: yearlyCountData.isApproximate
            )
            typeCount.yearlyCounts?.append(yearlyCount)
        }

        return typeCount
    }
}

private extension SnapshotFetchProgress {
    func updateDetails(from result: TypeCountFetchResult) {
        updateDetails(
            result.typeIdentifier,
            quality: result.diagnosticQuality,
            recordCount: result.count,
            isUnsupported: result.isUnsupported,
            authorizationStatus: result.authorizationStatus,
            earliestDate: result.earliestDate,
            latestDate: result.latestDate,
            yearlyCounts: result.yearlyCounts.map {
                SnapshotFetchProgress.YearlyCountProgress(
                    year: $0.year,
                    count: $0.count,
                    isApproximate: $0.isApproximate
                )
            },
            apiCallDetails: result.apiCalls,
            timeoutConfiguredSeconds: result.timeoutConfiguredSeconds,
            totalElapsedSeconds: result.totalElapsedSeconds,
            timeoutMode: result.timeoutMode,
            lastSuccessfulElapsed: result.lastSuccessfulElapsed,
            learnedTimeout: result.learnedTimeout,
            suggestedRetryTimeout: result.suggestedRetryTimeout,
            timeoutCount: result.timeoutCount,
            successCount: result.successCount
        )
    }

    func updateTimeoutProfile(from profile: MetricTimeoutProfile, for typeID: String) {
        updateTimeoutProfile(
            typeID,
            timeoutMode: profile.timeoutMode,
            lastSuccessfulElapsed: profile.lastSuccessfulElapsed,
            learnedTimeout: profile.effectiveTimeout,
            suggestedRetryTimeout: profile.suggestedRetryTimeout,
            timeoutCount: profile.timeoutCount,
            successCount: profile.successCount
        )
    }
}

private enum DistributionCaptureConfiguration {
    static let bucketComponent: Calendar.Component = .day
    static let bucketStep = 1
}
