//
// ExerciseLibraryView.swift
// Workouts
//
// Copyright 2026 Rouslan Zenetl. All Rights Reserved.
//

import SwiftUI
import SwiftData

/// The Exercises segment of the Library tab: every bundled exercise (the same
/// catalog the picker offers), grouped into `ExerciseCatalog`'s curated sections and
/// searchable by name, category, or target muscle. Tapping a row opens its reference
/// page. Pushed from `LibraryView`, so it relies on that view's `NavigationStack`.
struct ExerciseLibraryView: View {
    @State private var searchText = ""

    private var sections: [ExerciseCatalogSection] {
        ExerciseCatalog.sections(matching: searchText)
    }

    var body: some View {
        Group {
            if sections.isEmpty {
                ContentUnavailableView.search(text: searchText)
            } else {
                List {
                    ForEach(sections, id: \.title) { section in
                        Section(section.title) {
                            ForEach(section.names, id: \.self) { name in
                                NavigationLink(name) {
                                    ExerciseLibraryDetailView(exerciseName: name)
                                }
                            }
                        }
                    }
                }
            }
        }
        .navigationTitle("\(ExerciseMotionLibrary.exerciseNames.count) Exercises")
        .searchable(
            text: $searchText,
            placement: .navigationBarDrawer(displayMode: .always),
            prompt: "Name, category, or muscle")
    }
}

/// One library exercise, in the standard half-and-half exercise layout: the top half
/// scrolls the user's machine settings (their setup cheat-sheet), the authored
/// reference content (summary, targets, setup/execution/cues…), and the weight
/// progression chart; the looping form-guide figure holds the bottom.
struct ExerciseLibraryDetailView: View {
    @Environment(SyncEngine.self) private var sync
    @Environment(AppServices.self) private var services

    let exerciseName: String

    /// This exercise wherever it appears in the user's routines — the source of the
    /// recorded machine settings.
    @Query private var exercises: [Exercise]

    /// Presents the settings editor; carries the routines the edit writes back to.
    @State private var settingsEdit: SettingsEditRoute?

    /// True while content extends below the scroll area — drives the bottom fade
    /// that makes the reference copy read as scrollable.
    @State private var moreBelow = false

    /// One completed weighted log is enough to justify the progression chart;
    /// bodyweight and timed exercises never accrue one, so they skip the chart
    /// instead of plotting a flat zero line.
    @Query private var weightedLogs: [WorkoutLog]

    init(exerciseName: String) {
        self.exerciseName = exerciseName
        let name = exerciseName
        _exercises = Query(filter: #Predicate<Exercise> { $0.name == name })
        let completedRaw = WorkoutStatus.completed.rawValue
        _weightedLogs = Query(filter: #Predicate<WorkoutLog> {
            $0.exerciseName == name && $0.statusRaw == completedRaw && $0.weight > 0
        })
    }

    private var info: ExerciseInfo? {
        ExerciseInfoLibrary.info(for: exerciseName)
    }

    /// Distinct recorded machine-settings lists for this exercise. Usually one; when
    /// routines disagree, each distinct list keeps its routine's name as a label. Each
    /// group carries every routine showing that list, so an edit updates them all and
    /// they stay deduped.
    private var settingsGroups: [SettingsGroup] {
        var groups: [SettingsGroup] = []
        for exercise in exercises {
            guard let settings = exercise.machineSettings, !settings.isEmpty else { continue }
            if let i = groups.firstIndex(where: { $0.settings == settings }) {
                if let id = exercise.routine?.id { groups[i].routineIDs.append(id) }
                continue
            }
            groups.append(SettingsGroup(
                label: exercise.routine?.name,
                settings: settings,
                routineIDs: exercise.routine.map { [$0.id] } ?? []
            ))
        }
        if groups.count == 1 { groups[0].label = nil }
        return groups
    }

    /// Every routine containing this exercise — the write targets when settings are
    /// recorded here for the first time.
    private var containingRoutineIDs: [String] {
        exercises.compactMap { $0.routine?.id }
    }

    var body: some View {
        VStack(spacing: 0) {
            ScrollView {
                VStack(alignment: .leading, spacing: 16) {
                    // Show the card whenever settings are recorded — or, for an
                    // exercise the authored info identifies as machine-based, as an
                    // empty state, so the feature is discoverable before first use.
                    if !settingsGroups.isEmpty {
                        MachineSettingsCard(groups: settingsGroups, onEdit: edit(group:))
                    } else if info?.isMachineBased == true {
                        MachineSettingsCard(
                            groups: [],
                            onEdit: edit(group:),
                            // Recording needs a routine exercise to write to — without
                            // one the card explains instead of offering a dead button.
                            onAdd: containingRoutineIDs.isEmpty ? nil : {
                                settingsEdit = SettingsEditRoute(initial: [], targetRoutineIDs: containingRoutineIDs)
                            }
                        )
                    }
                    if let info {
                        ExerciseInfoContent(info: info)
                    }
                    if !weightedLogs.isEmpty {
                        WeightProgressionChartView(exerciseName: exerciseName)
                    }
                }
                .padding()
                .frame(maxWidth: .infinity, alignment: .leading)
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            // Fade the last lines while more content lies below, so the reference
            // copy visibly continues past the figure boundary; gone at scroll end.
            .onScrollGeometryChange(for: Bool.self) { geo in
                geo.contentOffset.y + geo.containerSize.height < geo.contentSize.height - 12
            } action: { _, more in
                withAnimation(.easeInOut(duration: 0.2)) { moreBelow = more }
            }
            .overlay(alignment: .bottom) {
                if moreBelow {
                    LinearGradient(
                        colors: [Color(.systemBackground).opacity(0), Color(.systemBackground)],
                        startPoint: .top, endPoint: .bottom
                    )
                    .frame(height: 28)
                    .allowsHitTesting(false)
                    .transition(.opacity)
                }
            }

            ExerciseFigureSlot(exerciseName: exerciseName)
        }
        .navigationTitle(exerciseName)
        .navigationBarTitleDisplayMode(.inline)
        .toolbar {
            if let info {
                ToolbarItem(placement: .primaryAction) {
                    let announcer = services.speechAnnouncer
                    Button {
                        if announcer.isSpeaking {
                            announcer.stop()
                        } else {
                            announcer.speak(info.spokenScript(name: exerciseName, brief: false))
                        }
                    } label: {
                        Image(systemName: announcer.isSpeaking ? "stop.fill" : "speaker.wave.2.fill")
                    }
                    .accessibilityLabel(announcer.isSpeaking ? "Stop reading" : "Read instructions aloud")
                }
            }
        }
        // Leaving the screen shouldn't keep narrating from a page you can no longer see.
        .onDisappear { services.speechAnnouncer.stop() }
        .sheet(item: $settingsEdit) { route in
            MachineSettingsSheet(exerciseName: exerciseName, initialSettings: route.initial) { updated in
                let targets = route.targetRoutineIDs
                Task {
                    for routineID in targets {
                        await sync.writeBackMachineSettings(updated, exerciseName: exerciseName, routineID: routineID)
                    }
                }
            }
        }
    }

    private func edit(group: SettingsGroup) {
        settingsEdit = SettingsEditRoute(initial: group.settings, targetRoutineIDs: group.routineIDs)
    }
}

/// One distinct recorded settings list and the routines it came from (the edit targets).
fileprivate struct SettingsGroup {
    var label: String?
    var settings: [MachineSetting]
    var routineIDs: [String]
}

/// Sheet route: the settings to seed the editor with and the routines a save writes to.
private struct SettingsEditRoute: Identifiable {
    let id = UUID()
    var initial: [MachineSetting]
    var targetRoutineIDs: [String]
}

/// The user's recorded machine comfort settings, set apart from the reference text
/// as a card: name–value rows, grouped per routine when the recorded lists differ.
/// Empty `groups` renders the not-yet-recorded state (shown for machine-based
/// exercises so the feature is visible before first use), with an Add button when
/// the exercise lives in at least one routine (`onAdd` non-nil). A single group edits
/// from the header; disagreeing groups edit per group.
private struct MachineSettingsCard: View {
    let groups: [SettingsGroup]
    var onEdit: (SettingsGroup) -> Void
    var onAdd: (() -> Void)? = nil

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            HStack {
                Text("Your Machine Settings")
                    .font(.headline)
                Spacer()
                if groups.count == 1 {
                    Button("Edit") { onEdit(groups[0]) }
                        .font(.subheadline)
                } else if groups.isEmpty, let onAdd {
                    Button("Add") { onAdd() }
                        .font(.subheadline)
                }
            }
            if groups.isEmpty {
                Text(onAdd != nil
                     ? "None recorded yet — save the seat, pad, and pin positions once and they'll be here next time."
                     : "None recorded yet. Add this exercise to a routine, then record its settings here or from the workout screen.")
                    .font(.callout)
                    .foregroundStyle(.secondary)
            }
            ForEach(Array(groups.enumerated()), id: \.offset) { _, group in
                VStack(alignment: .leading, spacing: 4) {
                    if let label = group.label {
                        HStack {
                            Text(label)
                                .font(.caption)
                                .foregroundStyle(.secondary)
                            Spacer()
                            if groups.count > 1 {
                                Button("Edit") { onEdit(group) }
                                    .font(.caption)
                            }
                        }
                    }
                    ForEach(Array(group.settings.enumerated()), id: \.offset) { _, setting in
                        HStack(alignment: .firstTextBaseline) {
                            Text(setting.name)
                                .foregroundStyle(.secondary)
                            Spacer()
                            Text(setting.value)
                                .fontWeight(.semibold)
                                .monospacedDigit()
                        }
                        .font(.callout)
                    }
                }
            }
        }
        .padding(12)
        .frame(maxWidth: .infinity, alignment: .leading)
        .background(Color(.secondarySystemBackground), in: RoundedRectangle(cornerRadius: 12))
    }
}

