//
// WorkoutLogsView.swift
// Workouts
//
// Created by rzen on 7/13/25 at 6:52 PM.
//
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
//

import IndieSync
import SwiftUI
import SwiftData

struct WorkoutLogsView: View {
    @Environment(SyncEngine.self) private var sync
    @Environment(AppServices.self) private var services

    @Query(sort: \Workout.start, order: .reverse)
    private var workouts: [Workout]

    @State private var showingSplitPicker = false
    @State private var showingSettings = false
    @State private var itemToDelete: Workout?
    /// ID of the just-started workout; drives the push into its log screen once the
    /// cache observer delivers the entity a beat after the file write (see
    /// `navigatesToStartedWorkout`).
    @State private var pendingWorkoutID: String?

    // WorkoutLogsView is the app's root screen, so it owns its NavigationStack.
    var body: some View {
        NavigationStack {
            List {
                ForEach(workouts) { workout in
                    NavigationLink {
                        WorkoutLogListView(workout: workout)
                    } label: {
                        CalendarListItem(
                            date: workout.start,
                            title: workout.splitName ?? Split.unnamed,
                            subtitle: getSubtitle(for: workout),
                            subtitle2: workout.statusName
                        )
                    }
                    .swipeActions(edge: .trailing, allowsFullSwipe: false) {
                        Button {
                            itemToDelete = workout
                        } label: {
                            Label("Delete", systemImage: "trash")
                        }
                        .tint(.red)
                    }
                }
            }
            .overlay {
                if workouts.isEmpty {
                    ContentUnavailableView(
                        "No Workouts Yet",
                        systemImage: "list.bullet.clipboard",
                        description: Text("Start a new workout from one of your splits.")
                    )
                }
            }
            .navigationTitle("Workout Logs")
            // Write-queue banner tiers (calm "pending" capsule / prominent fault).
            // On the root screen so a stalled save is visible where editing happens.
            .safeAreaInset(edge: .bottom) {
                SyncStatusBanner()
            }
            .toolbar {
                ToolbarItem(placement: .navigationBarLeading) {
                    Button {
                        showingSettings = true
                    } label: {
                        Image(systemName: "gearshape.2")
                    }
                    .accessibilityLabel("Settings")
                }
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button("Start New") {
                        showingSplitPicker.toggle()
                    }
                }
            }
            .sheet(isPresented: $showingSettings) {
                SettingsView()
            }
            .sheet(isPresented: $showingSplitPicker) {
                SplitPickerSheet { pendingWorkoutID = $0 }
            }
            // Once "Start New" saves a workout, drop straight into its log screen —
            // the same landing the split's exercise-list start path uses.
            .navigatesToStartedWorkout(pendingWorkoutID: $pendingWorkoutID)
            .confirmationDialog(
                "Delete Workout?",
                isPresented: Binding(
                    get: { itemToDelete != nil },
                    set: { if !$0 { itemToDelete = nil } }
                ),
                titleVisibility: .visible,
                presenting: itemToDelete
            ) { workout in
                Button("Delete", role: .destructive) {
                    Task { await sync.delete(workout: workout) }
                    itemToDelete = nil
                }
                // Only offered when the workout actually has a Health record. Capture
                // the UUID before the delete prunes the cache entity.
                if let healthUUID = workout.metricHealthKitWorkoutUUID {
                    Button("Delete + Remove from Apple Health", role: .destructive) {
                        services.workoutHealthDeleter.deleteFromHealth(uuidString: healthUUID)
                        Task { await sync.delete(workout: workout) }
                        itemToDelete = nil
                    }
                }
                Button("Cancel", role: .cancel) {
                    itemToDelete = nil
                }
            } message: { workout in
                if workout.metricSourceRaw == MetricSource.watch.rawValue {
                    Text("This workout was recorded by Apple Watch; if it can't be removed from Apple Health here, delete it in the Health app.")
                }
            }
        }
    }

    private func getSubtitle(for workout: Workout) -> String {
        if workout.status == .completed, let endDate = workout.end {
            return workout.start.humanTimeInterval(to: endDate)
        } else {
            return workout.start.formattedDate()
        }
    }
}

// MARK: - Split Picker Sheet

struct SplitPickerSheet: View {
    @Environment(SyncEngine.self) private var sync
    @Environment(AppServices.self) private var services
    @Environment(\.dismiss) private var dismiss

    /// Called with the new workout's id right after it's saved, so the presenting
    /// screen can navigate into it once the cache catches up.
    var onStarted: (String) -> Void = { _ in }

    @Query(sort: [SortDescriptor(\Split.order), SortDescriptor(\Split.name)])
    private var splits: [Split]

    @Query(sort: \Workout.start, order: .reverse)
    private var workouts: [Workout]

    /// Set when the user picks a split while other workouts are still going — drives the
    /// "end the current one(s) or run in parallel?" prompt.
    @State private var splitAwaitingConfirmation: Split?

    private var activeWorkouts: [Workout] {
        workouts.filter { $0.status == .inProgress || $0.status == .notStarted }
    }

    /// The splits most recently trained, newest first, each with the start date
    /// of its latest workout — derived from the workout log (`workouts` is
    /// already sorted by start descending). Each workout's `splitID` follows the
    /// seed clone-on-edit redirect so a workout started from a since-forked seed
    /// still credits the live clone. Deduped, capped, and limited to splits that
    /// still exist.
    private var recentSplits: [(split: Split, lastStart: Date)] {
        var seen = Set<String>()
        var recents: [(split: Split, lastStart: Date)] = []
        for workout in workouts {
            guard let splitID = workout.splitID else { continue }
            let liveID = sync.currentSplitID(for: splitID)
            guard !seen.contains(liveID) else { continue }
            seen.insert(liveID)
            if let split = splits.first(where: { $0.id == liveID }) {
                recents.append((split, workout.start))
                if recents.count == 3 { break }
            }
        }
        return recents
    }

    /// One selectable split row, shared by the Recent and All Splits sections.
    /// Recent rows pass `lastTrained` to show how long ago the split was run.
    private func splitRow(_ split: Split, lastTrained: Date? = nil) -> some View {
        Button {
            confirmAndStart(with: split)
        } label: {
            HStack {
                Image(systemName: split.systemImage)
                    .foregroundColor(Color.color(from: split.color))
                    .frame(width: 28)
                VStack(alignment: .leading, spacing: 2) {
                    Text(split.name)
                    if let lastTrained {
                        Text(lastTrained.daysAgoLabel())
                            .font(.caption)
                            .foregroundColor(.secondary)
                    }
                }
                Spacer()
                Text("\(split.exercisesArray.count) exercises")
                    .font(.caption)
                    .foregroundColor(.secondary)
            }
        }
    }

    var body: some View {
        NavigationStack {
            List {
                if !recentSplits.isEmpty {
                    Section("Recent") {
                        ForEach(recentSplits, id: \.split.id) { recent in
                            splitRow(recent.split, lastTrained: recent.lastStart)
                        }
                    }
                }

                Section(recentSplits.isEmpty ? "" : "All Splits") {
                    ForEach(splits) { split in
                        splitRow(split)
                    }
                }
            }
            .navigationTitle("Select a Split")
            .toolbar {
                ToolbarItem(placement: .navigationBarLeading) {
                    Button("Cancel") {
                        dismiss()
                    }
                }
            }
            .confirmationDialog(
                activePromptTitle,
                isPresented: Binding(
                    get: { splitAwaitingConfirmation != nil },
                    set: { if !$0 { splitAwaitingConfirmation = nil } }
                ),
                titleVisibility: .visible,
                presenting: splitAwaitingConfirmation
            ) { split in
                Button("End Current & Start New") { endActiveThenStart(with: split) }
                Button("Start in Parallel") { start(with: split) }
                Button("Cancel", role: .cancel) { splitAwaitingConfirmation = nil }
            } message: { _ in
                Text(activePromptMessage)
            }
        }
    }

    private var activePromptTitle: String {
        activeWorkouts.count == 1 ? "Workout in Progress" : "\(activeWorkouts.count) Workouts in Progress"
    }

    private var activePromptMessage: String {
        let n = activeWorkouts.count
        let those = n == 1 ? "it" : "them"
        return "You already have \(n == 1 ? "a workout" : "\(n) workouts") going. End \(those) first, or run this one alongside."
    }

    /// Prompt before starting if other workouts are still going; otherwise start straight away.
    private func confirmAndStart(with split: Split) {
        if activeWorkouts.isEmpty {
            start(with: split)
        } else {
            splitAwaitingConfirmation = split
        }
    }

    /// End every in-flight workout (keeping its progress), then start the picked split.
    private func endActiveThenStart(with split: Split) {
        let toEnd = activeWorkouts.map { WorkoutDocument(from: $0) }
        splitAwaitingConfirmation = nil
        Task {
            for var doc in toEnd {
                doc.endKeepingProgress()
                await sync.save(workout: doc)
            }
        }
        start(with: split)
    }

    private func start(with split: Split) {
        let startDate = Date()
        let logs = split.exercisesArray.enumerated().map { index, exercise in
            WorkoutLogDocument(planFrom: ExerciseDocument(from: exercise), order: index, date: startDate)
        }

        // A freshly started workout has no `end` — only completion stamps it.
        let doc = WorkoutDocument(
            schemaVersion: WorkoutDocument.currentSchemaVersion,
            id: ULID.make(),
            splitID: split.id,
            splitName: split.name,
            start: startDate,
            end: nil,
            status: WorkoutStatus.notStarted.rawValue,
            createdAt: startDate,
            updatedAt: startDate,
            logs: logs
        )

        // Hand the id back after the save so the presenter can poll the cache and
        // navigate into the run — mirroring the exercise-list start path.
        Task {
            await sync.save(workout: doc)
            onStarted(doc.id)
        }
        // Bring the Apple Watch up into the session so the user can run it from the wrist,
        // tagged with the split's activity type so the saved Health workout is categorized.
        services.workoutLauncher.launchWatchWorkout(activityType: split.activityTypeEnum.hkActivityType)
        dismiss()
    }
}

// MARK: - Started-Workout Navigation

/// Drives a programmatic push into a freshly-started workout's log screen. Both start
/// paths — the split picker sheet and a split's exercise list — mint a `WorkoutDocument`,
/// write it, then hand its id here; the workout row only materializes once the
/// file→observer→cache loop round-trips, so we poll the cache for the entity and push it
/// the moment it lands.
private struct StartedWorkoutNavigator: ViewModifier {
    @Environment(\.modelContext) private var modelContext
    @Binding var pendingWorkoutID: String?
    @State private var resolvedWorkout: Workout?

    func body(content: Content) -> some View {
        content
            .navigationDestination(item: $resolvedWorkout) { workout in
                WorkoutLogListView(workout: workout)
            }
            .onChange(of: pendingWorkoutID) { _, id in
                guard let id else { return }
                pollForWorkout(id: id)
            }
    }

    /// Poll for the entity after we write the document (the file→observer→cache loop
    /// typically completes in well under a second). Clear the pending id once it
    /// resolves, or silently after ~3 s if it never arrives.
    private func pollForWorkout(id: String) {
        Task {
            for _ in 0..<20 {
                try? await Task.sleep(for: .milliseconds(150))
                if let workout = CacheMapper.fetchWorkout(id: id, in: modelContext) {
                    resolvedWorkout = workout
                    pendingWorkoutID = nil
                    return
                }
            }
            pendingWorkoutID = nil
        }
    }
}

extension View {
    /// Navigate into a workout's log screen once it appears in the cache after being
    /// started. Bind to the state you set to the new workout's id right after saving it.
    func navigatesToStartedWorkout(pendingWorkoutID: Binding<String?>) -> some View {
        modifier(StartedWorkoutNavigator(pendingWorkoutID: pendingWorkoutID))
    }
}
