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

import SwiftUI
import SwiftData

/// The Exercises library screen, pushed from Settings → Library: every exercise with
/// a bundled motion rig (the same catalog the picker offers), each opening its
/// reference page.
struct ExerciseLibraryView: View {
    var body: some View {
        List(ExerciseMotionLibrary.exerciseNames, id: \.self) { name in
            NavigationLink(name) {
                ExerciseLibraryDetailView(exerciseName: name)
            }
        }
        .navigationTitle("\(ExerciseMotionLibrary.exerciseNames.count) Exercises")
    }
}

/// 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))
    }
}

/// Renders a parsed `ExerciseInfo`: summary, target chips, then each authored
/// section with its numbered steps or bullets. Internal so the debug figure-review
/// screen can reuse it.
struct ExerciseInfoContent: View {
    let info: ExerciseInfo

    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            if !info.summary.isEmpty {
                Text(info.summary)
                    .font(.callout)
                    .foregroundStyle(.secondary)
            }

            if !info.targets.isEmpty {
                TargetChips(targets: info.targets)
            }

            ForEach(info.sections, id: \.title) { section in
                VStack(alignment: .leading, spacing: 6) {
                    Text(section.title)
                        .font(.headline)
                    ForEach(Array(section.items.enumerated()), id: \.offset) { index, item in
                        itemRow(item, ordinal: ordinal(for: item, at: index, in: section))
                    }
                }
            }
        }
    }

    /// Steps number themselves by their position among the section's steps, so an
    /// interleaved paragraph doesn't break the count.
    private func ordinal(for item: ExerciseInfo.Item, at index: Int, in section: ExerciseInfo.Section) -> Int? {
        guard case .step = item else { return nil }
        return section.items[...index].reduce(0) { count, it in
            if case .step = it { count + 1 } else { count }
        }
    }

    @ViewBuilder
    private func itemRow(_ item: ExerciseInfo.Item, ordinal: Int?) -> some View {
        switch item {
        case .step(let text):
            HStack(alignment: .firstTextBaseline, spacing: 6) {
                Text("\(ordinal ?? 0).")
                    .foregroundStyle(.secondary)
                    .monospacedDigit()
                Text(text)
            }
            .font(.callout)
        case .bullet(let text):
            HStack(alignment: .firstTextBaseline, spacing: 6) {
                Text("•")
                    .foregroundStyle(.secondary)
                Text(text)
            }
            .font(.callout)
        case .paragraph(let text):
            Text(text)
                .font(.callout)
        }
    }
}

/// The `Targets:` muscles as wrapping capsule chips.
private struct TargetChips: View {
    let targets: [String]

    var body: some View {
        FlowLayout(spacing: 6) {
            ForEach(targets, id: \.self) { target in
                Text(target)
                    .font(.caption)
                    .padding(.horizontal, 10)
                    .padding(.vertical, 4)
                    .background(Color.accentColor.opacity(0.12), in: Capsule())
                    .foregroundStyle(Color.accentColor)
            }
        }
    }
}

/// Minimal leading-aligned wrapping layout for the target chips.
private struct FlowLayout: Layout {
    var spacing: CGFloat = 6

    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        let rows = layoutRows(maxWidth: proposal.width ?? .infinity, subviews: subviews)
        let height = rows.map(\.height).reduce(0, +) + spacing * CGFloat(max(0, rows.count - 1))
        return CGSize(width: proposal.width ?? rows.map(\.width).max() ?? 0, height: height)
    }

    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
        var y = bounds.minY
        var index = 0
        for row in layoutRows(maxWidth: bounds.width, subviews: subviews) {
            var x = bounds.minX
            for size in row.sizes {
                subviews[index].place(
                    at: CGPoint(x: x, y: y),
                    proposal: ProposedViewSize(size)
                )
                x += size.width + spacing
                index += 1
            }
            y += row.height + spacing
        }
    }

    private struct Row {
        var sizes: [CGSize] = []
        var width: CGFloat = 0
        var height: CGFloat = 0
    }

    private func layoutRows(maxWidth: CGFloat, subviews: Subviews) -> [Row] {
        var rows: [Row] = []
        var current = Row()
        for subview in subviews {
            let size = subview.sizeThatFits(.unspecified)
            let next = current.sizes.isEmpty ? size.width : current.width + spacing + size.width
            if next > maxWidth, !current.sizes.isEmpty {
                rows.append(current)
                current = Row()
            }
            current.sizes.append(size)
            current.width = current.sizes.isEmpty ? 0 : current.width + (current.sizes.count == 1 ? size.width : spacing + size.width)
            current.height = max(current.height, size.height)
        }
        if !current.sizes.isEmpty { rows.append(current) }
        return rows
    }
}
