//
// ExerciseView.swift
// Workouts
//
// Created by rzen on 7/18/25 at 5:44 PM.
//
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
//

import SwiftUI
import SwiftData
import Charts

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

    let workout: Workout
    let logID: String

    /// Working copy of the parent workout. Driving the UI from local state (not the
    /// cache entity) lets `seedDoc` show a just-added log before the file→cache
    /// round-trip catches up.
    @State private var doc: WorkoutDocument
    @State private var showingPlanEdit = false
    @State private var showingNotesEdit = false
    @State private var showingMachineSettings = false

    /// `seedDoc` lets the caller hand over an in-memory document (e.g. the parent's
    /// working copy right after adding an exercise) so the screen doesn't wait on
    /// the file→cache round-trip to find the just-created log.
    init(workout: Workout, logID: String, seedDoc: WorkoutDocument? = nil) {
        self.workout = workout
        self.logID = logID
        _doc = State(initialValue: seedDoc ?? WorkoutDocument(from: workout))
    }

    /// The log being edited within the working doc.
    private var log: WorkoutLogDocument? {
        doc.logs.first { $0.id == logID }
    }

    var body: some View {
        Group {
            if let log {
                // Deliberately NOT the half-and-half figure layout: this screen is
                // dense with the plan, notes, settings, and the progression chart —
                // the form guide lives in the run flow and the exercise library.
                content(for: log)
            } else {
                // The just-added log hasn't reached the cache yet; refresh shortly.
                ProgressView()
            }
        }
        .navigationTitle(log?.exerciseName ?? "")
        .sheet(isPresented: $showingPlanEdit) {
            PlanEditView(workout: workout, logID: logID)
        }
        .sheet(isPresented: $showingNotesEdit) {
            NotesEditView(workout: workout, logID: logID)
        }
        .sheet(isPresented: $showingMachineSettings) {
            if let log {
                MachineSettingsSheet(
                    exerciseName: log.exerciseName,
                    initialSettings: log.machineSettings ?? []
                ) { updated in
                    applyMachineSettings(updated)
                }
            }
        }
        .onAppear {
            refreshDocIfNeeded()
            // Take over this run: the watch parks and locks it while we're editing
            // here. Guarded — even `id` is a persisted property and traps when read
            // on an entity a remote delete has already reaped.
            if workout.isLive {
                services.watchBridge.setEditingWorkout(workout.id)
            }
        }
        .onDisappear {
            services.watchBridge.setEditingWorkout(nil)
        }
        // Reflect external changes (e.g. a set completed on the watch) live. Each edit
        // rewrites the whole workout file, so the cache always holds the latest — pulling
        // it in can't revert a newer local tap. The observed expression is evaluated on
        // every body pass, so it must be liveness-guarded like any persisted read.
        .onChange(of: workout.isLive ? workout.updatedAt : nil) { _, _ in
            absorbExternalUpdate()
        }
    }

    /// Pull an externally-changed workout into the working copy so the plan and notes
    /// update without leaving and re-entering the screen. Skipped if the entity died
    /// (remote delete) — mapping a dead @Model traps.
    private func absorbExternalUpdate() {
        if let fresh = WorkoutDocument(fromLive: workout) { doc = fresh }
    }

    @ViewBuilder
    private func content(for log: WorkoutLogDocument) -> some View {
        Form {
            // MARK: - Plan Section (Read-only with Edit button)
            Section {
                PlanTilesView(log: log)
            } header: {
                HStack {
                    Text("Plan")
                    Spacer()
                    Button("Edit") {
                        showingPlanEdit = true
                    }
                    .font(.subheadline)
                    .textCase(.none)
                }
            }

            // MARK: - Machine Settings — shown when the log carries settings, or for
            // any exercise the authored library identifies as machine-based, so the
            // section is discoverable before settings are first recorded.
            if log.machineSettings != nil
                || ExerciseInfoLibrary.info(for: log.libraryExerciseName)?.isMachineBased == true {
                let settings = log.machineSettings ?? []
                Section {
                    if settings.isEmpty {
                        Text("No settings recorded")
                            .foregroundColor(.secondary)
                            .italic()
                    } else {
                        ForEach(Array(settings.enumerated()), id: \.offset) { _, setting in
                            HStack(alignment: .firstTextBaseline) {
                                Text(setting.name)
                                    .foregroundStyle(.secondary)
                                Spacer()
                                Text(setting.value)
                                    .fontWeight(.semibold)
                                    .monospacedDigit()
                            }
                        }
                    }
                } header: {
                    HStack {
                        Text("Machine Settings")
                        Spacer()
                        Button("Edit") {
                            showingMachineSettings = true
                        }
                        .font(.subheadline)
                        .textCase(.none)
                    }
                }
            }

            // MARK: - Notes Section (Read-only with Edit button)
            Section {
                if let notes = log.notes, !notes.isEmpty {
                    Text(notes)
                        .foregroundColor(.primary)
                } else {
                    Text("No notes")
                        .foregroundColor(.secondary)
                        .italic()
                }
            } header: {
                HStack {
                    Text("Notes")
                    Spacer()
                    Button("Edit") {
                        showingNotesEdit = true
                    }
                    .font(.subheadline)
                    .textCase(.none)
                }
            }

            // MARK: - Progress Tracking Chart (weighted exercises only — a weight
            // chart is meaningless for bodyweight/timed logs)
            if LoadType(rawValue: log.loadType) == .weight {
                Section(header: Text("Progress Tracking")) {
                    WeightProgressionChartView(exerciseName: log.exerciseName)
                }
            }
        }
        // Pull plan/notes edits made in the sheets back into the live doc.
        .onChange(of: showingPlanEdit) { _, presenting in
            if !presenting { refreshDocFromCache() }
        }
        .onChange(of: showingNotesEdit) { _, presenting in
            if !presenting { refreshDocFromCache() }
        }
    }

    /// Apply edited machine settings: update the log's plan-time snapshot (persisted
    /// with the workout), then write the same settings back to the originating
    /// routine's exercise as the durable default — same sequencing as the workout
    /// list's machine sheet.
    private func applyMachineSettings(_ settings: [MachineSetting]) {
        guard let i = doc.logs.firstIndex(where: { $0.id == logID }) else { return }
        doc.logs[i].machineSettings = settings
        doc.logs[i].touch()
        doc.updatedAt = Date()
        let snapshot = doc
        let exerciseName = doc.logs[i].exerciseName
        let routineID = doc.routineID
        Task {
            await sync.save(workout: snapshot)
            await sync.writeBackMachineSettings(settings, exerciseName: exerciseName, routineID: routineID)
        }
    }

    /// If the requested log isn't in the working doc yet (just-added race), pull a
    /// fresh copy from the cache entity once it catches up.
    private func refreshDocIfNeeded() {
        guard log == nil else { return }
        refreshDocFromCache()
    }

    /// Re-read the workout from the cache to absorb edits made by child sheets
    /// (plan/notes).
    private func refreshDocFromCache() {
        doc = WorkoutDocument(from: workout)
    }
}

// MARK: - Plan Tiles View

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

    var body: some View {
        if LoadType(rawValue: log.loadType) == .duration {
            // Duration layout: Sets | Duration
            HStack(spacing: 0) {
                PlanTile(label: "Sets", value: "\(log.sets)")
                PlanTile(label: "Duration", value: formattedDuration)
            }
        } else {
            // Weight layout: Sets | Reps | Weight
            HStack(spacing: 0) {
                PlanTile(label: "Sets", value: "\(log.sets)")
                PlanTile(label: "Reps", value: "\(log.reps)")
                PlanTile(label: "Weight", value: weightUnit.format(log.weight))
            }
        }
    }

    private var formattedDuration: String {
        let mins = log.durationSeconds / 60
        let secs = log.durationSeconds % 60
        if mins > 0 && secs > 0 {
            return "\(mins)m \(secs)s"
        } else if mins > 0 {
            return "\(mins) min"
        } else if secs > 0 {
            return "\(secs) sec"
        } else {
            return "0 sec"
        }
    }
}

struct PlanTile: View {
    let label: String
    let value: String

    var body: some View {
        VStack(spacing: 4) {
            Text(label)
                .font(.caption)
                .foregroundColor(.secondary)
            Text(value)
                .font(.title2)
                .fontWeight(.semibold)
                .foregroundColor(.primary)
        }
        .frame(maxWidth: .infinity)
        .padding(.vertical, 12)
        .background(Color(.systemGray6))
        .cornerRadius(8)
    }
}
