import SwiftUI
import Collections
import HealthKit
import HealthDB
import HealthKitExtensions
import CoreLocation

struct WorkoutDetailView: View {

    @EnvironmentObject
    var database: Database

    private let store = HealthStore()

    let workout: Workout

    private let heartRateCategoryCount = 110

    @State
    private var healthWorkout: HKWorkout?

    @State
    private var heartRateSamples: [HeartRate] = []

    @State
    private var samples: [HRSample] = []

    @State
    private var locationSamples: [CLLocation] = []

    @State
    private var privateMetadata: [(key: String, value: Any)] = []

    @State
    private var showErrorMessage = false

    @State
    private var errorMessage: String = ""

    @State
    private var isProcessingWorkout = false

    @State
    private var healthButtonText = "Checking for workout in Health..."

    private var metadataFields: [(key: String, value: Any)] {
        workout.metadata?.sorted { $0.key } ?? []
    }

    private var averageHeartRate: Int {
        let sum = heartRateSamples.reduce(0) { $0 + $1.beatsPerMinute }
        return (Double(sum) / Double(heartRateSamples.count)).roundedInt
    }

    var body: some View {
        List {
            Button(action: addOrDeleteHealthWorkout) {
                HStack {
                    Spacer()
                    if isProcessingWorkout {
                        ProgressView()
                            .progressViewStyle(.circular)
                            .padding(.trailing, 10)
                    }
                    VStack {
                        Text(healthButtonText)
                            .foregroundStyle(.accent)
                        if healthWorkout != nil {
                            Text("Tap to delete")
                                .font(.caption)
                                .foregroundStyle(.gray)
                        }
                    }
                    Spacer()
                }
                .padding(.vertical, 8)
            }
            .disabled(isProcessingWorkout)
            Section("Info") {
                DetailRow("Start", date: workout.startDate)
                DetailRow("Duration", duration: workout.duration)
                DetailRow("Total Distance", kilometer: workout.totalDistance)
                if let goal = workout.goal {
                    DetailRow("Goal", value: goal)
                }
            }
            Section {
                DisclosureGroup {
                    ForEach(workout.workoutActivities, id: \.startDate) { activity in
                        NavigationLink(value: activity) {
                            DetailRow(activity.workoutConfiguration.activityType.description,
                                      time: activity.startDate)
                        }
                    }
                } label: {
                    DetailRow("Activities", value: workout.workoutActivities.count)
                }
                DisclosureGroup {
                    ForEach(workout.workoutEvents) { event in
                        NavigationLink(value: event) {
                            DetailRow(event.type.description, time: event.dateInterval.start)
                        }
                    }
                } label: {
                    DetailRow("Events", value: workout.workoutEvents.count)
                }
                DisclosureGroup {
                    ForEach(metadataFields, id:\.key) { (key, value) in
                        DetailRow(MetadataKeyName(key), value: "\(value)")
                    }
                } label: {
                    DetailRow("Metadata", value: workout.metadata?.count ?? 0)
                }
                DisclosureGroup {
                    ForEach(privateMetadata, id:\.key) { (key, value) in
                        DetailRow(MetadataKeyName(key), value: "\(value)")
                    }
                } label: {
                    DetailRow("Private Metadata", value: privateMetadata.count)
                }
            }
            if !heartRateSamples.isEmpty {
                Section("Heart Rate") {
                    VStack(alignment: .leading) {
                        HeartRateGraph(measurements: samples)
                            .frame(height: 110)
                        HStack {
                            Text("\(averageHeartRate) BPM AVG")
                                .foregroundStyle(.red)
                                .fontWeight(.semibold)
                            Spacer()
                            Text("\(heartRateSamples.count) samples")
                                .foregroundStyle(.secondary)
                        }
                        .font(.caption)
                        .textCase(.uppercase)
                    }
                }
            }
            if !locationSamples.isEmpty {
                Section("Route") {
                    Text("")
                        .frame(height: 150)
                        .listRowBackground(WorkoutMapView(locations: locationSamples))
                        .overlay(
                            NavigationLink(value: locationSamples) { }
                                .opacity(0))
                }
            }
        }
        .listStyle(SidebarListStyle())
        .navigationTitle(workout.typeString)
        .navigationDestination(for: HKWorkoutActivity.self) { activity in
            ActivityDetailView(workout: workout, activity: activity)
        }
        .navigationDestination(for: HKWorkoutEvent.self) { event in
            EventDetailView(event: event)
        }
        .navigationDestination(for: [CLLocation].self) { locations in
            LocationSampleListView(samples: locations)
        }
        .onAppear(perform: loadSamples)
        .alert("Processing error", isPresented: $showErrorMessage) {
            Button("Dismiss", role: .cancel) { }
        } message: {
            Text(errorMessage)
        }
    }

    private func show(_ error: String) {
        DispatchQueue.main.async {
            self.errorMessage = error
            self.showErrorMessage = true
        }
    }

    private func updateButtonText() {
        if isProcessingWorkout {
            if healthWorkout == nil {
                self.healthButtonText = "Adding workout to Health..."
            } else {
                self.healthButtonText = "Deleting workout from Health..."
            }
        } else {
            if healthWorkout == nil {
                healthButtonText = "Add workout to Health"
            } else {
                healthButtonText = "Found matching workout in Health"
            }
        }
    }

    private func addOrDeleteHealthWorkout() {
        guard !isProcessingWorkout else {
            return
        }
        DispatchQueue.main.async {
            self.isProcessingWorkout = true
            updateButtonText()
        }
        Task {
            if let healthWorkout {
                await delete(healthWorkout: healthWorkout)
            } else {
                await addWorkoutToHealth()
            }
            DispatchQueue.main.async {
                self.isProcessingWorkout = false
                updateButtonText()
            }
        }
    }

    private func delete(healthWorkout: HKWorkout) async {
        do {
            try await store.store.delete(healthWorkout)
            DispatchQueue.main.async {
                self.healthWorkout = nil
                self.isProcessingWorkout = false
            }
        } catch {
            show("Failed to delete workout: \(error)")
            DispatchQueue.main.async {
                self.isProcessingWorkout = false
            }
        }
    }

    private func addWorkoutToHealth() async {
        guard let db = database.store else {
            return
        }
        await insert(workout: workout, using: db)
    }

    private func requestAuthorization() async -> Bool {
        do {
            try await store.requestAuthorization(
                toShare: HKWorkout.self, HKWorkoutRoute.self,
                read: HKWorkout.self, HKWorkoutRoute.self)
            return true
        } catch {
            show("Failed to check for workout permissions: \(error)")
            return false
        }
    }

    private func insert(workout: Workout, using db: HealthDatabase) async {
        guard await requestAuthorization() else { return }
        if store.authorizationStatus(for: HKWorkout.self) == .notDetermined ||
            store.authorizationStatus(for: HKWorkoutRoute.self) == .notDetermined {
            print("Requesting workout permissions")
            guard await requestAuthorization() else { return }
        }
        guard store.authorizationStatus(for: HKWorkout.self) == .sharingAuthorized else {
            show("No sharing permission for workouts")
            return
        }
        guard store.authorizationStatus(for: HKWorkoutRoute.self) == .sharingAuthorized else {
            show("No sharing permission for workout routes")
            return
        }
        var samples: [HKSample]
        do {
            samples = try db.store.samples(associatedWith: workout)
        } catch {
            show("Failed to access samples associated with workout: \(error)")
            return
        }

        // Add missing samples from statistics
        let newSamples = makeStatisticSamples(workout, in: db)
        for sample in newSamples {
            guard !samples.contains(where: { $0.sampleType == sample.sampleType }) else {
                continue
            }
            let typeName = HKQuantityTypeIdentifier(rawValue: sample.quantityType.identifier).description
            print("Adding missing sample \(typeName)")
            samples.append(sample)
        }

        let route: WorkoutRoute?
        do {
            route = try db.route(associatedWith: workout)
        } catch {
            show("Failed to get route associated with workout: \(error)")
            return
        }
        let locations: [CLLocation]
        do {
            locations = try route.map {
                try db.locations(associatedWith: $0)
            } ?? []
        } catch {
            show("Failed to get locations associated with route: \(error)")
            return
        }
        print("Saving workout in Health: \(samples.count) samples, \(locations.count) locations")
        let savedWorkout: HKWorkout
        do {
            savedWorkout = try await workout.insert(
                into: store.store,
                samples: samples,
                route: locations)
        } catch {
            show("Failed to insert workout: \(error)")
            return
        }
        DispatchQueue.main.async {
            self.healthWorkout = savedWorkout
        }
    }

    private func makeStatisticSamples(_ workout: Workout, in db: HealthDatabase) -> [HKQuantitySample] {
        let startDate = workout.startDate
        let endDate = workout.endDate
        let activity = workout.workoutActivities[0]

        do {
            let statistics = try db.statistics(associatedWith: activity)
            return statistics.map {
                .init(type: $0, quantity: $1.average, start: startDate, end: endDate)
            }
        } catch {
            print("Failed to get statistics for activity: \(error)")
            return []
        }
    }

    private func loadSamples() {
        Task {
            await checkPermissionsAndSearchHealth()
        }
        guard let db = database.store else { return }
        Task {
            await loadHeartRateSamples(db: db)
            await loadLocationSamples(db: db)
            await loadPrivateMetadata(db: db)
        }
    }

    private func loadHeartRateSamples(db: HealthDatabase) async {
        do {
            let samples: [HeartRate] = try db.samples(associatedWith: workout)
            let graphSamples = HRSample.create(from: samples, start: workout.startDate, end: workout.endDate, categories: heartRateCategoryCount)
            DispatchQueue.main.async {
                self.heartRateSamples = samples
                self.samples = graphSamples
            }
        } catch {
            show("Failed to load heart rate samples from database: \(error)")
        }
    }

    private func loadLocationSamples(db: HealthDatabase) async {
        do {
            guard let route = try db.route(associatedWith: workout) else {
                print("No route associated with workout")
                return
            }
            let locations = try db.locations(associatedWith: route)
            DispatchQueue.main.async {
                self.locationSamples = locations
            }
            print("Loaded \(locations.count) locations from database")
        } catch {
            show("Failed to load locations or route from database: \(error)")
        }
    }

    private func loadPrivateMetadata(db: HealthDatabase) async {
        do {
            let metadata = try db.store.metadata(for: workout.uuid, includePrivateMetadata: true)
                .filter { $0.key.hasPrefix("_HKPrivate") }
            DispatchQueue.main.async {
                self.privateMetadata = metadata.sorted { $0.key }
            }
        } catch {
            show("Failed to load private metadata from database: \(error)")
        }
    }

    private func checkPermissionsAndSearchHealth() async {
        guard HKHealthStore.isHealthDataAvailable() else {
            return
        }
        DispatchQueue.main.async {
            self.healthButtonText = "Checking for matching workout..."
            self.isProcessingWorkout = true
        }
        do {
            try await checkPermissionsAndFindWorkout()
        } catch {
            show("Failed to search for similar workout in Health: \(error)")
        }
        DispatchQueue.main.async {
            self.isProcessingWorkout = false
            updateButtonText()
        }
    }

    private func checkPermissionsAndFindWorkout() async throws {
        switch  store.authorizationStatus(for: HKWorkout.self) {
        case .notDetermined:
            try await requestWorkoutPermission()
            try await checkPermissionsAndFindWorkout()
        case .sharingAuthorized, .sharingDenied:
            await findWorkoutInHealth()
            return
        @unknown default:
            show("Unknown permission for workouts")
            return
        }
    }

    private func requestWorkoutPermission() async throws {
        try await store.requestAuthorization(read: HKWorkout.self)
    }

    private func findWorkoutInHealth() async {
        guard let activityType = workout.workoutActivities.first?.workoutConfiguration.activityType else {
            show("No activity type associated with workout")
            return
        }

        let start = workout.startDate.addingTimeInterval(-60)
        let end = workout.endDate.addingTimeInterval(60)
        do {
            guard let workout = try await store.workouts(activityType: activityType, from: start, to: end)
                .first else {
                print("No matching workout found in Health")
                return
            }

            DispatchQueue.main.async {
                self.healthWorkout = workout
            }
        } catch {
            show("Failed to search for matching workout: \(error)")
        }
    }
}

#Preview {
    return NavigationStack {
        WorkoutDetailView(workout: .mock1)
            .environmentObject(Database.empty)
            .preferredColorScheme(.dark)
    }
}

extension String: @retroactive Identifiable {
    
    public var id: Self { self }
}

private extension Array where Element == HeartRate {

    var minimumHeartRate: Int {
        self.min {
            $0.value < $1.value
        }?.beatsPerMinute ?? 0
    }

    var maximumHeartRate: Int {
        self.max {
            $0.value < $1.value
        }?.beatsPerMinute ?? 0
    }
}

private extension HeartRate {

    var beatsPerMinute: Int {
        quantity.doubleValue(for: .count().unitDivided(by: .minute())).roundedInt
    }
}

extension HeartRate {

    var sampleWithoutPrivateMetadata: HeartRate {
        .init(quantity: quantity,
              start: startDate,
              end: endDate,
              uuid: externalUUID,
              device: device,
              metadata: metadata?.removingPrivateFields())
    }
}
