//
// WorkoutLogListView.swift
// Workouts Watch App
//
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
//

import IndieSync
import SwiftUI
import SwiftData

struct WorkoutLogListView: View {
    @Environment(WatchConnectivityBridge.self) private var bridge
    @Environment(WorkoutSessionManager.self) private var sessionManager
    @Environment(\.modelContext) private var modelContext

    /// Working copy of the workout. We drive the UI from this and mutate it on
    /// every edit (then forward through the bridge) to avoid the read-after-write
    /// race against the cache, which lags local writes by a beat.
    @State private var doc: WorkoutDocument

    /// The live cache entity, kept only to observe `updatedAt` for the absorb
    /// below — all rendering and editing goes through the `doc` snapshot.
    private let workout: Workout

    @State private var showingExercisePicker = false
    @State private var selectedLogID: String?

    init(workout: Workout) {
        self.workout = workout
        // A closure-based `NavigationLink` builds this destination eagerly for every
        // row in the parent list — including during the update that fires when a phone
        // push removes a workout — so map only a live entity; a deleted one traps in
        // SwiftData. The placeholder is never shown (the row is on its way out).
        _doc = State(initialValue: WorkoutDocument(fromLive: workout) ?? .deletedPlaceholder)
    }

    /// The routine this workout came from (read-only on the watch), used to offer
    /// additional exercises that aren't logged yet. Fetched imperatively — *not* via
    /// `@Query` — so the list body never observes the live `Routine` or traverses its
    /// `exercises` relationship during a render. Doing so (a `@Query`-observed model
    /// whose to-many relationship is read in `body`) drove a SwiftData re-render loop
    /// that hung the watch. `availableExercises` is therefore only ever evaluated from
    /// the picker sheet's closure, not from `body`.
    private var routine: Routine? {
        guard let routineID = doc.routineID else { return nil }
        return CacheMapper.fetchRoutine(id: routineID, in: modelContext)
    }

    private var sortedLogs: [WorkoutLogDocument] {
        doc.logs.sorted { $0.order < $1.order }
    }

    private var availableExercises: [Exercise] {
        guard let routine else { return [] }
        let existingNames = Set(doc.logs.map { $0.exerciseName })
        return routine.exercisesArray.filter { !existingNames.contains($0.name) }
    }

    var body: some View {
        List {
            if sessionManager.isRunning {
                Section { LiveMetricsHUD() }
            }

            Section(header: Text(label)) {
                ForEach(sortedLogs) { log in
                    Button {
                        selectedLogID = log.id
                    } label: {
                        WorkoutLogRowLabel(log: log)
                    }
                    .buttonStyle(.plain)
                }
            }

            // Gate on the *resolved* routine, not the stale `routineID` string — a routine
            // deleted on the phone leaves the id dangling, and offering Add Exercise
            // against it would only show a misleading "All exercises added" picker.
            if routine != nil {
                Section {
                    Button {
                        showingExercisePicker = true
                    } label: {
                        HStack {
                            Image(systemName: "plus.circle.fill")
                                .foregroundColor(.green)
                            Text("Add Exercise")
                        }
                    }
                }
            }
        }
        .overlay {
            if sortedLogs.isEmpty {
                ContentUnavailableView(
                    "No Exercises",
                    systemImage: "figure.strengthtraining.traditional",
                    description: Text(routine == nil
                        ? "No exercises in this workout."
                        : "Tap + to add exercises.")
                )
            }
        }
        .navigationTitle(doc.routineName ?? Routine.unnamed)
        // Absorb phone edits while this screen is open (H1's watch-side gap):
        // without this, the snapshot stays frozen at init, and the next watch
        // tick — made against a state that's missing the phone's edit — would be
        // stamped fresh and clobber it wholesale on the phone. Strictly-newer
        // guard only: an equal timestamp is our own edit echoing back through
        // the phone's push, and a stale push in flight (the phone pushed before
        // ingesting our latest edit) must not regress what the user is looking
        // at. Clock skew across devices degrades this to a skipped absorb —
        // never to a clobber of local state.
        .onChange(of: workout.isLive ? workout.updatedAt : nil) { _, incoming in
            // ...and never absorb from an entity a push has just deleted: the observed
            // expression above is evaluated on every body pass, so even it must be
            // liveness-guarded — reading a persisted property on a dead @Model traps.
            if let incoming, incoming > doc.updatedAt,
               let fresh = WorkoutDocument(fromLive: workout) {
                doc = fresh
            }
        }
        .navigationDestination(item: $selectedLogID) { logID in
            // RunFlowView hosts the run and, in a flow-mode routine, auto-advances across
            // exercises. It owns the live-run mirror wiring (incoming-frame filter,
            // `navigatedRunID`, and the parameterized live-ended signal) since the on-screen
            // log advances during a flowing run.
            RunFlowView(
                doc: $doc,
                startLogID: logID,
                onChange: { bridge.update(workout: doc) },
                onLive: { bridge.sendLiveProgress($0) },
                sendLiveEnded: { lid in bridge.sendLiveEnded(workoutID: doc.id, logID: lid) }
            )
        }
        .sheet(isPresented: $showingExercisePicker) {
            ExercisePickerView(exercises: availableExercises) { exercise in
                addExercise(exercise)
            }
        }
    }

    private var label: String {
        let start = doc.start
        if doc.status == WorkoutStatus.completed.rawValue, let end = doc.end {
            if start.isSameDay(as: end) {
                return "\(start.formattedDate())—\(end.formattedTime())"
            } else {
                return "\(start.formattedDate())—\(end.formattedDate())"
            }
        }
        return start.formattedDate()
    }

    private func addExercise(_ exercise: Exercise) {
        var newLog = WorkoutLogDocument(
            id: ULID.make(),
            exerciseName: exercise.name,
            order: doc.logs.count,
            sets: exercise.sets,
            reps: exercise.reps,
            weight: exercise.weight,
            loadType: exercise.loadType,
            durationSeconds: exercise.durationTotalSeconds,
            currentStateIndex: 0,
            status: WorkoutStatus.notStarted.rawValue,
            notes: nil,
            date: doc.start,
            libraryName: exercise.libraryName
        )
        newLog.touch()   // a fresh log carries a definite creation time for the merge
        doc.logs.append(newLog)
        doc.updatedAt = Date()
        bridge.update(workout: doc)
        showingExercisePicker = false
    }
}

// MARK: - Live Metrics HUD

/// Compact live readout (heart rate + active calories) shown while a watch session is
/// running. Reads the session manager from the environment itself so its per-second
/// updates re-render only this row, not the editable log list above it. Renders
/// nothing until the first sample arrives, and dims under Always-On like the timers.
private struct LiveMetricsHUD: View {
    @Environment(WorkoutSessionManager.self) private var sessionManager
    @Environment(\.isLuminanceReduced) private var dimmed

    var body: some View {
        let heartRate = sessionManager.currentHeartRate
        let energy = sessionManager.currentActiveEnergyKcal
        if heartRate != nil || energy != nil {
            HStack(spacing: 16) {
                if let heartRate {
                    Label("\(Int(heartRate.rounded()))", systemImage: "heart.fill")
                        .foregroundStyle(dimmed ? Color.secondary : Color.red)
                }
                if let energy {
                    Label("\(Int(energy.rounded())) cal", systemImage: "flame.fill")
                        .foregroundStyle(dimmed ? Color.secondary : Color.orange)
                }
            }
            .font(.caption.monospacedDigit())
            .frame(maxWidth: .infinity)
        }
    }
}

// MARK: - Workout Log Row Label

struct WorkoutLogRowLabel: View {
    let log: WorkoutLogDocument
    @AppStorage("weightUnit") private var weightUnit: WeightUnit = .lb

    var body: some View {
        HStack {
            statusIcon
                .foregroundColor(statusColor)

            VStack(alignment: .leading, spacing: 2) {
                Text(log.exerciseName)
                    .font(.headline)
                    .lineLimit(1)

                Text(subtitle)
                    .font(.caption2)
                    .foregroundColor(.secondary)
            }

            Spacer()
        }
    }

    private var status: WorkoutStatus {
        WorkoutStatus(rawValue: log.status) ?? .notStarted
    }

    private var statusIcon: Image {
        switch status {
        case .completed:
            Image(systemName: "checkmark.circle.fill")
        case .inProgress:
            Image(systemName: "circle.dotted")
        case .notStarted:
            Image(systemName: "circle")
        case .skipped:
            Image(systemName: "xmark.circle")
        }
    }

    private var statusColor: Color {
        switch status {
        case .completed:
            .accentColor
        case .inProgress:
            .gray
        case .notStarted:
            .secondary
        case .skipped:
            .secondary
        }
    }

    private var subtitle: String {
        if LoadType(rawValue: log.loadType) == .duration {
            let mins = log.durationSeconds / 60
            let secs = log.durationSeconds % 60
            if mins > 0 && secs > 0 {
                return "\(log.sets) × \(mins)m \(secs)s"
            } else if mins > 0 {
                return "\(log.sets) × \(mins) min"
            } else {
                return "\(log.sets) × \(secs) sec"
            }
        } else {
            return "\(log.sets) × \(log.reps) × \(weightUnit.format(log.weight))"
        }
    }
}

// MARK: - Exercise Picker View

struct ExercisePickerView: View {
    @Environment(\.dismiss) private var dismiss

    let exercises: [Exercise]
    let onSelect: (Exercise) -> Void
    @AppStorage("weightUnit") private var weightUnit: WeightUnit = .lb

    var body: some View {
        NavigationStack {
            List {
                if exercises.isEmpty {
                    Text("All exercises added")
                        .foregroundColor(.secondary)
                } else {
                    ForEach(exercises) { exercise in
                        Button {
                            onSelect(exercise)
                            dismiss()
                        } label: {
                            VStack(alignment: .leading) {
                                Text(exercise.name)
                                    .font(.headline)
                                Text(exerciseSubtitle(exercise))
                                    .font(.caption2)
                                    .foregroundColor(.secondary)
                            }
                        }
                    }
                }
            }
            .navigationTitle("Add Exercise")
            .toolbar {
                ToolbarItem(placement: .cancellationAction) {
                    Button("Cancel") {
                        dismiss()
                    }
                }
            }
        }
    }

    private func exerciseSubtitle(_ exercise: Exercise) -> String {
        if exercise.loadTypeEnum == .duration {
            let mins = exercise.durationMinutes
            let secs = exercise.durationSeconds
            if mins > 0 && secs > 0 {
                return "\(exercise.sets) × \(mins)m \(secs)s"
            } else if mins > 0 {
                return "\(exercise.sets) × \(mins) min"
            } else {
                return "\(exercise.sets) × \(secs) sec"
            }
        } else {
            return "\(exercise.sets) × \(exercise.reps) × \(weightUnit.format(exercise.weight))"
        }
    }
}
