import Foundation
import HealthKit

struct HealthProfileImportDraft: Identifiable, Equatable {
    let id = UUID()
    var birthYear: Int?
    var biologicalSexRaw: String?
    var heightCM: Int?
    var bloodTypeRaw: String?

    var hasAnyImportableField: Bool {
        birthYear != nil ||
        biologicalSexRaw != nil ||
        heightCM != nil ||
        bloodTypeRaw != nil
    }

    func apply(to profile: UserProfile, now: Date = .now) {
        if let birthYear { profile.birthYear = birthYear }
        if let biologicalSexRaw { profile.biologicalSexRaw = biologicalSexRaw }
        if let heightCM { profile.heightCM = heightCM }
        if let bloodTypeRaw { profile.bloodTypeRaw = bloodTypeRaw }
        profile.updatedAt = now
    }
}

struct HealthProfileImportPreview {
    struct Field: Equatable {
        let title: String
        let current: String
        let imported: String?

        var willUpdate: Bool {
            guard let imported else { return false }
            return imported != current
        }
    }

    let birthYear: Field
    let sex: Field
    let height: Field
    let bloodType: Field

    var fields: [Field] { [birthYear, sex, height, bloodType] }

    init(draft: HealthProfileImportDraft, current profile: UserProfile) {
        birthYear = Field(
            title: String(appLoc: "出生年份"),
            current: profile.birthYear.map(String.init) ?? String(appLoc: "未设置"),
            imported: draft.birthYear.map(String.init)
        )
        sex = Field(
            title: String(appLoc: "性别"),
            current: profile.sex.label,
            imported: draft.biologicalSexRaw.map { Self.sexLabel(raw: $0) }
        )
        height = Field(
            title: String(appLoc: "身高"),
            current: profile.heightCM.map { "\($0)cm" } ?? String(appLoc: "未设置"),
            imported: draft.heightCM.map { "\($0)cm" }
        )
        bloodType = Field(
            title: String(appLoc: "血型"),
            current: profile.bloodTypeRaw.isEmpty ? String(appLoc: "未设置") : "\(profile.bloodTypeRaw)型",
            imported: draft.bloodTypeRaw.map { "\($0)型" }
        )
    }

    private static func sexLabel(raw: String) -> String {
        (UserProfile.Sex(rawValue: raw) ?? .undisclosed).label
    }
}

enum HealthProfileImportError: LocalizedError {
    case unavailable
    case noReadableFields

    var errorDescription: String? {
        switch self {
        case .unavailable:
            return String(appLoc: "这台设备暂不支持读取 Apple 健康资料。")
        case .noReadableFields:
            return String(appLoc: "Apple 健康里没有可导入的生日、性别、身高或血型。")
        }
    }
}

struct HealthProfileImportService {
    static let shared = HealthProfileImportService()

    private let store = HKHealthStore()

    func fetchDraft() async throws -> HealthProfileImportDraft {
        guard HKHealthStore.isHealthDataAvailable() else {
            throw HealthProfileImportError.unavailable
        }

        let readTypes = readObjectTypes()
        try await requestReadAuthorization(for: readTypes)

        async let birthYear = readBirthYear()
        async let sex = readBiologicalSexRaw()
        async let height = readLatestHeightCM()
        async let bloodType = readBloodTypeRaw()

        let draft = HealthProfileImportDraft(
            birthYear: try await birthYear,
            biologicalSexRaw: try await sex,
            heightCM: try await height,
            bloodTypeRaw: try await bloodType
        )
        guard draft.hasAnyImportableField else {
            throw HealthProfileImportError.noReadableFields
        }
        return draft
    }

    private func readObjectTypes() -> Set<HKObjectType> {
        var types: Set<HKObjectType> = [
            HKObjectType.characteristicType(forIdentifier: .dateOfBirth)!,
            HKObjectType.characteristicType(forIdentifier: .biologicalSex)!,
            HKObjectType.characteristicType(forIdentifier: .bloodType)!,
        ]
        if let height = HKObjectType.quantityType(forIdentifier: .height) {
            types.insert(height)
        }
        return types
    }

    private func requestReadAuthorization(for readTypes: Set<HKObjectType>) async throws {
        try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
            store.requestAuthorization(toShare: [], read: readTypes) { _, error in
                if let error {
                    continuation.resume(throwing: error)
                } else {
                    continuation.resume()
                }
            }
        }
    }

    private func readBirthYear() throws -> Int? {
        try store.dateOfBirthComponents().year
    }

    private func readBiologicalSexRaw() throws -> String? {
        switch try store.biologicalSex().biologicalSex {
        case .female: return "female"
        case .male: return "male"
        default: return nil
        }
    }

    private func readBloodTypeRaw() throws -> String? {
        switch try store.bloodType().bloodType {
        case .aPositive, .aNegative: return "A"
        case .bPositive, .bNegative: return "B"
        case .abPositive, .abNegative: return "AB"
        case .oPositive, .oNegative: return "O"
        default: return nil
        }
    }

    private func readLatestHeightCM() async throws -> Int? {
        guard let heightType = HKObjectType.quantityType(forIdentifier: .height) else {
            return nil
        }

        let sort = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false)
        return try await withCheckedThrowingContinuation { continuation in
            let query = HKSampleQuery(
                sampleType: heightType,
                predicate: nil,
                limit: 1,
                sortDescriptors: [sort]
            ) { _, samples, error in
                if let error {
                    continuation.resume(throwing: error)
                    return
                }
                let sample = samples?.first as? HKQuantitySample
                let cm = sample?.quantity.doubleValue(for: .meterUnit(with: .centi))
                continuation.resume(returning: cm.map { Int($0.rounded()) })
            }
            store.execute(query)
        }
    }
}
