//
// ExerciseProgressView.swift
// Workouts
//
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
//

import SwiftUI
import UIKit

/// Runs a single exercise as a horizontally-paged flow — the iPhone counterpart to the
/// watch's `ExerciseProgressView`:
///
///   [Ready] → Work₁ → Rest₁ → Work₂ → … → Workₙ → Finish
///
/// A **Ready?** page leads in *only* when the exercise hasn't been started yet (a
/// resumed exercise jumps straight to its first unfinished set). The **work** phase
/// counts *up* (a stopwatch for the current set, tinted with the brand purple); the
/// user swipes left when they're done. The **rest** phase counts *down* from the
/// configurable rest time (light teal), buzzes once per second in the final three
/// seconds, then auto-advances to the next work phase. Sliding past the final set
/// reaches a **Finish** page offering **One More** (append a bonus set) and **Done**
/// (which also auto-fires after a configurable countdown, completing the exercise).
///
/// The paged flow occupies the **top half** of the screen; the bottom half is reserved
/// blank for a later iteration. A row of phase dots tracks progress: purple for work,
/// teal for rest, with the current phase drawn as a wider dash.
struct ExerciseProgressView: View {
    @Environment(\.dismiss) private var dismiss

    /// The shared working workout document owned by the parent list. We mutate the
    /// matching log in place and ask the parent to persist each change — driving the UI
    /// from this doc (not the cache) avoids losing rapid edits to the read-after-write
    /// race the cache update lags behind.
    @Binding var doc: WorkoutDocument
    let logID: String
    let onChange: () -> Void

    /// Rest length between sets, shared with the watch via the same defaults key.
    @AppStorage("restSeconds") private var restSeconds: Int = 45

    /// Planned set count for this run. `One More` bumps it (and the log's `sets`).
    @State private var setCount: Int
    @State private var currentPage: Int
    @State private var didRestorePage = false

    /// True when this run opens on the lead-in **Ready?** page (the exercise hadn't
    /// been started yet). Held in `@State` so it stays fixed for the life of the screen:
    /// the parent list rebuilds (re-inits) this view whenever the workout file changes,
    /// and once Start marks the exercise in-progress a recomputed `let` would flip to
    /// `false` mid-run — dropping `base` from 1 to 0 and remapping the current page onto
    /// the wrong phase. Frozen here, all the page-index math below stays stable.
    @State private var showsReady: Bool

    init(doc: Binding<WorkoutDocument>, logID: String, onChange: @escaping () -> Void) {
        self._doc = doc
        self.logID = logID
        self.onChange = onChange

        let log = doc.wrappedValue.logs.first { $0.id == logID }
        let sets = max(1, log?.sets ?? 1)
        _setCount = State(initialValue: sets)

        let notStarted = (log?.status ?? WorkoutStatus.notStarted.rawValue) == WorkoutStatus.notStarted.rawValue
        _showsReady = State(initialValue: notStarted)

        let base = notStarted ? 1 : 0
        // Resume on the first unfinished set's work page (clamped to the last set).
        let completed = min(max(0, log?.currentStateIndex ?? 0), sets - 1)
        let resume = notStarted ? 0 : base + completed * 2
        _currentPage = State(initialValue: resume)
    }

    private var log: WorkoutLogDocument? {
        doc.logs.first { $0.id == logID }
    }

    /// Offset of the work/rest cycle: `1` when a Ready page leads, else `0`.
    private var base: Int { showsReady ? 1 : 0 }

    /// Work/rest pages: Work₁, Rest₁, …, Workₙ ⇒ N sets + (N−1) rests = 2N − 1.
    private var cycleCount: Int { setCount * 2 - 1 }

    /// Ready (`base`) + cycle (`2N − 1`) + Finish (`1`).
    private var totalPages: Int { base + cycleCount + 1 }

    /// The first unfinished set's work page (only used when resuming, so `base == 0`).
    private var resumePage: Int {
        let completed = min(max(0, log?.currentStateIndex ?? 0), setCount - 1)
        return base + completed * 2
    }

    /// Position within the work/rest cycle for the current page — `nil` on the Ready
    /// and Finish pages (which show no dots).
    private var currentCycleIndex: Int? {
        let c = currentPage - base
        return (0..<cycleCount).contains(c) ? c : nil
    }

    /// Work-only progress model for the dot row: one marker per set, a dash on the set
    /// being worked, and a doubled gap straddling the rest you're currently in.
    private var workDots: WorkPhaseDots.Model? {
        guard let c = currentCycleIndex else { return nil }
        if c.isMultiple(of: 2) {
            let set = c / 2
            return .init(setCount: setCount, activeSet: set, restAfterSet: nil, completed: set)
        } else {
            let set = (c - 1) / 2
            return .init(setCount: setCount, activeSet: nil, restAfterSet: set, completed: set + 1)
        }
    }

    private var detail: String {
        guard let log else { return "" }
        if LoadType(rawValue: log.loadType) == .duration {
            return Self.durationLabel(log.durationSeconds)
        }
        return "\(log.reps) reps"
    }

    /// Timed exercise: the work phase counts *down* from its duration (and auto-advances),
    /// rather than counting *up* until the user swipes on.
    private var isDuration: Bool {
        guard let log else { return false }
        return LoadType(rawValue: log.loadType) == .duration
    }

    /// Per-set work duration for a timed exercise.
    private var workDurationSeconds: Int {
        max(1, log?.durationSeconds ?? 1)
    }

    /// One-line plan summary for the Ready page, e.g. "4 sets × 8 reps".
    private var readySummary: String {
        let setsText = "\(setCount) set\(setCount == 1 ? "" : "s")"
        return detail.isEmpty ? setsText : "\(setsText) × \(detail)"
    }

    var body: some View {
        VStack(spacing: 0) {
            // Paged flow — top half.
            TabView(selection: $currentPage) {
                ForEach(0..<totalPages, id: \.self) { index in
                    page(for: index)
                        .tag(index)
                }
            }
            .tabViewStyle(.page(indexDisplayMode: .never))
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .overlay(alignment: .bottom) {
                if let dots = workDots {
                    WorkPhaseDots(model: dots)
                        .padding(.bottom, 8)
                }
            }

            // Reserved space for a later iteration (set log, history, chart, …).
            Color.clear
                .frame(maxWidth: .infinity, maxHeight: .infinity)
        }
        .navigationTitle(log?.exerciseName ?? "")
        .navigationBarTitleDisplayMode(.inline)
        .onChange(of: currentPage) { _, newPage in
            // Swiping all the way back to the Ready page wipes the run; any other page
            // records forward progress.
            if showsReady && newPage == 0 {
                resetExercise()
            } else {
                recordProgress(for: newPage)
            }
        }
        .onAppear {
            // Keep the screen lit while logging — a mid-workout sleep is annoying.
            UIApplication.shared.isIdleTimerDisabled = true
            // Jump to the first unfinished set. A paged TabView can settle on page 0 on
            // first layout, so re-assert once more after this run loop. (The Ready page
            // pins page 0 itself, so skip the jump there.)
            guard !didRestorePage else { return }
            didRestorePage = true
            if !showsReady {
                jumpToResumePage()
                Task { @MainActor in jumpToResumePage() }
            }
        }
        .onDisappear {
            UIApplication.shared.isIdleTimerDisabled = false
        }
    }

    /// Move to the resume page without animation, only if we're not already there
    /// (so a re-assert after a TabView snap-to-0 is a no-op in the common case).
    private func jumpToResumePage() {
        let target = resumePage
        guard currentPage != target else { return }
        var transaction = Transaction()
        transaction.disablesAnimations = true
        withTransaction(transaction) { currentPage = target }
    }

    @ViewBuilder
    private func page(for index: Int) -> some View {
        let isActive = index == currentPage
        if showsReady && index == 0 {
            ReadyPhaseView(summary: readySummary, onStart: start)
        } else {
            let cycleIndex = index - base
            if cycleIndex == cycleCount {
                // Finish page — confirm Done (auto-fires) or add One More.
                FinishPhaseView(
                    isActive: isActive,
                    onDone: { completeExercise(); dismiss() },
                    onOneMore: addSet
                )
            } else if cycleIndex.isMultiple(of: 2) {
                let setNumber = cycleIndex / 2 + 1
                if isDuration {
                    // Timed work set — count down from the planned duration, then
                    // auto-advance (and buzz) the same way a rest does.
                    CountdownPhaseView(
                        header: "\(setNumber) of \(setCount)",
                        tint: .workTimer,
                        seconds: workDurationSeconds,
                        isActive: isActive
                    ) {
                        withAnimation { advance(from: index) }
                    }
                } else {
                    // Rep-based work set — count up; the user swipes left when done.
                    WorkPhaseView(
                        setNumber: setNumber,
                        totalSets: setCount,
                        detail: detail,
                        isActive: isActive
                    )
                }
            } else {
                // Rest phase. Auto-advances to the next work page when the timer hits zero.
                CountdownPhaseView(
                    header: "Rest",
                    tint: .restTimer,
                    seconds: restSeconds,
                    isActive: isActive
                ) {
                    withAnimation { advance(from: index) }
                }
            }
        }
    }

    // MARK: - Mutations

    /// Leave the Ready page for the first work phase, marking the exercise started.
    private func start() {
        beginExercise()
        withAnimation { currentPage = base }
    }

    /// Programmatically move one page right (used by the rest auto-advance), guarding
    /// against overrun if the user swiped away in the meantime.
    private func advance(from index: Int) {
        guard currentPage == index, index + 1 < totalPages else { return }
        currentPage = index + 1
    }

    /// Flip a not-yet-started exercise to in-progress the moment the user taps Start.
    private func beginExercise() {
        guard let i = doc.logs.firstIndex(where: { $0.id == logID }) else { return }
        guard doc.logs[i].status == WorkoutStatus.notStarted.rawValue else { return }
        doc.logs[i].status = WorkoutStatus.inProgress.rawValue
        recomputeWorkoutStatus()
        doc.updatedAt = Date()
        onChange()
    }

    /// Append a bonus set from the Finish page: mark every prior set done, grow the
    /// plan, and slide forward into the bonus set's work phase.
    private func addSet() {
        let newCount = setCount + 1

        if let i = doc.logs.firstIndex(where: { $0.id == logID }) {
            doc.logs[i].sets = newCount
            doc.logs[i].currentStateIndex = newCount - 1   // every prior set is now complete
            doc.logs[i].status = WorkoutStatus.inProgress.rawValue
            doc.logs[i].completed = false
            recomputeWorkoutStatus()
            doc.updatedAt = Date()
            onChange()
        }

        withAnimation {
            setCount = newCount
            currentPage += 1
        }
    }

    /// Map a page to completed-set count and record forward progress.
    ///
    /// Paging tops out at `setCount − 1` completed sets — the final set is marked done
    /// only by an explicit **Done** (`completeExercise`). Progress is **monotonic**:
    /// completing a work phase advances the count, but swiping back — or a transient
    /// TabView snap to page 0 — never un-counts a set.
    private func recordProgress(for pageIndex: Int) {
        if showsReady && pageIndex == 0 { return }   // Ready page records nothing
        let cycleIndex = pageIndex - base
        let reached = min(max(0, (cycleIndex + 1) / 2), setCount - 1)

        guard let i = doc.logs.firstIndex(where: { $0.id == logID }) else { return }
        guard reached > doc.logs[i].currentStateIndex else { return }

        doc.logs[i].currentStateIndex = reached
        doc.logs[i].status = WorkoutStatus.inProgress.rawValue
        doc.logs[i].completed = false

        recomputeWorkoutStatus()
        doc.updatedAt = Date()
        onChange()
    }

    /// Swiping back to the **Ready?** page starts the exercise over from scratch.
    private func resetExercise() {
        guard let i = doc.logs.firstIndex(where: { $0.id == logID }) else { return }
        let log = doc.logs[i]
        // Skip the write if it's already pristine (e.g. landing on Ready before any set).
        guard log.currentStateIndex != 0
                || log.status != WorkoutStatus.notStarted.rawValue
                || log.completed else { return }
        doc.logs[i].currentStateIndex = 0
        doc.logs[i].status = WorkoutStatus.notStarted.rawValue
        doc.logs[i].completed = false
        recomputeWorkoutStatus()
        doc.updatedAt = Date()
        onChange()
    }

    private func completeExercise() {
        guard let i = doc.logs.firstIndex(where: { $0.id == logID }) else { return }
        doc.logs[i].currentStateIndex = setCount
        doc.logs[i].status = WorkoutStatus.completed.rawValue
        doc.logs[i].completed = true

        recomputeWorkoutStatus()
        doc.updatedAt = Date()
        onChange()
    }

    private func recomputeWorkoutStatus() {
        let statuses = doc.logs.map { WorkoutStatus(rawValue: $0.status) ?? .notStarted }
        let allCompleted = !statuses.isEmpty && statuses.allSatisfy { $0 == .completed }
        let anyInProgress = statuses.contains { $0 == .inProgress }
        let allNotStarted = statuses.allSatisfy { $0 == .notStarted }

        if allCompleted {
            doc.status = WorkoutStatus.completed.rawValue
            doc.end = Date()
        } else if anyInProgress || !allNotStarted {
            doc.status = WorkoutStatus.inProgress.rawValue
            doc.end = nil
        } else {
            doc.status = WorkoutStatus.notStarted.rawValue
            doc.end = nil
        }
    }

    // MARK: - Formatting

    static func durationLabel(_ seconds: Int) -> String {
        let mins = seconds / 60
        let secs = seconds % 60
        if mins > 0 && secs > 0 { return "\(mins)m \(secs)s" }
        if mins > 0 { return "\(mins) min" }
        return "\(secs) sec"
    }
}

// MARK: - Haptics

/// Maps the watch flow's haptic vocabulary onto UIKit feedback generators so the iPhone
/// flow buzzes at the same beats (set start, countdown ping, rest end, done).
private enum WorkoutHaptic {
    case start, tick, stop, success

    @MainActor
    func play() {
        switch self {
        case .start:
            UIImpactFeedbackGenerator(style: .medium).impactOccurred()
        case .tick:
            UIImpactFeedbackGenerator(style: .light).impactOccurred()
        case .stop:
            UIImpactFeedbackGenerator(style: .rigid).impactOccurred()
        case .success:
            UINotificationFeedbackGenerator().notificationOccurred(.success)
        }
    }
}

// MARK: - Phase Colors

private extension Color {
    /// Count-up work tint — brand purple, brightened in dark mode for contrast on
    /// black and deepened in light mode for contrast on white.
    static let workTimer = Color(UIColor { traits in
        traits.userInterfaceStyle == .dark
            ? UIColor(red: 0.66, green: 0.45, blue: 0.96, alpha: 1)
            : UIColor(red: 0.45, green: 0.18, blue: 0.78, alpha: 1)
    })

    /// Count-down rest tint — a light gray that deepens in light mode so it stays
    /// legible on a white background.
    static let restTimer = Color(UIColor { traits in
        traits.userInterfaceStyle == .dark
            ? UIColor(white: 0.74, alpha: 1)
            : UIColor(white: 0.52, alpha: 1)
    })
}

// MARK: - Phase Timer Layout

/// Shared skeleton for the work and rest pages so their timers use an identical font
/// and land at exactly the same spot: a header line, the big timer, then a footer line.
/// The footer reserves its height even when empty, keeping the timer centered the same
/// way on both pages.
private struct PhaseTimerLayout<Content: View>: View {
    let header: String
    let footer: String
    let tint: Color
    @ViewBuilder var timer: Content

    var body: some View {
        VStack(spacing: 10) {
            Text(header)
                .font(.title3)
                .foregroundStyle(.secondary)

            timer
                .font(.system(size: 108, weight: .bold, design: .rounded))
                .monospacedDigit()
                .foregroundStyle(tint)
                .lineLimit(1)
                .minimumScaleFactor(0.5)

            Text(footer.isEmpty ? " " : footer)
                .font(.title3)
                .foregroundStyle(.secondary)
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

// MARK: - Work Phase Dots

/// Progress row with one marker per work set. The set being worked is drawn as a wider
/// dash; during a rest every marker is a plain dot and the gap straddling that rest
/// grows to about double, hinting at the pause between the set that just ended and the
/// next one. Completed sets are full strength, upcoming ones dimmed.
private struct WorkPhaseDots: View {
    struct Model: Equatable {
        let setCount: Int
        /// The set currently being worked — drawn as a dash. `nil` during a rest.
        let activeSet: Int?
        /// The set just completed; the gap *after* its dot doubles. `nil` during work.
        let restAfterSet: Int?
        /// How many sets are fully done (for dimming the upcoming ones).
        let completed: Int
    }

    let model: Model

    // Geometry — tune freely.
    private let dotWidth: CGFloat = 8
    private let dashWidth: CGFloat = 20
    private let markerHeight: CGFloat = 8
    private let gap: CGFloat = 8
    private var restGap: CGFloat { gap * 2 }

    var body: some View {
        HStack(spacing: 0) {
            ForEach(0..<model.setCount, id: \.self) { i in
                marker(for: i)
                if i < model.setCount - 1 {
                    Color.clear.frame(width: gapWidth(after: i), height: markerHeight)
                }
            }
        }
        .animation(.easeInOut(duration: 0.3), value: model)
    }

    private func marker(for i: Int) -> some View {
        let isActive = model.activeSet == i
        let isDone = i < model.completed
        return Capsule()
            .fill(Color.workTimer)
            .frame(width: isActive ? dashWidth : dotWidth, height: markerHeight)
            .opacity(isActive || isDone ? 1 : 0.45)
    }

    private func gapWidth(after i: Int) -> CGFloat {
        model.restAfterSet == i ? restGap : gap
    }
}

// MARK: - Phase Button Styling

private extension View {
    /// Chunky, rounded, heavy treatment shared by the Start / Done / One More buttons:
    /// a plump label (echoing the counter digits) over a taller full-width body.
    func phaseButtonLabel() -> some View {
        self
            .font(.system(.title2, design: .rounded, weight: .heavy))
            .frame(maxWidth: .infinity)
            .padding(.vertical, 14)
    }
}

// MARK: - Ready Phase

private struct ReadyPhaseView: View {
    let summary: String
    let onStart: () -> Void

    var body: some View {
        VStack(spacing: 14) {
            Text("Ready?")
                .font(.system(size: 44, weight: .bold, design: .rounded))

            if !summary.isEmpty {
                Text(summary)
                    .font(.title3)
                    .foregroundStyle(.secondary)
            }

            Button(action: onStart) {
                Text("Start")
                    .phaseButtonLabel()
            }
            .buttonStyle(.borderedProminent)
            .tint(.workTimer)
            .buttonBorderShape(.capsule)
            .padding(.top, 8)
            .padding(.horizontal, 40)
        }
        .padding(.horizontal)
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

// MARK: - Work Phase

private struct WorkPhaseView: View {
    let setNumber: Int
    let totalSets: Int
    let detail: String
    let isActive: Bool

    /// Wall-clock anchor for the count-up stopwatch. Driving the display from a fixed
    /// start date (rendered by SwiftUI's timer text) instead of incrementing a counter
    /// keeps it advancing without a run-loop timer.
    @State private var startDate = Date()

    var body: some View {
        PhaseTimerLayout(header: "\(setNumber) of \(totalSets)", footer: detail, tint: .workTimer) {
            Text(startDate, style: .timer)
        }
        .onAppear { if isActive { restart() } }
        .onChange(of: isActive) { _, active in if active { restart() } }
    }

    private func restart() {
        startDate = Date()
        WorkoutHaptic.start.play()
    }
}

// MARK: - Countdown Phase

/// A count-down phase used for both rests and timed work sets: counts down from
/// `seconds`, pings once per second in the final three, then buzzes and auto-advances at
/// zero. The header/tint distinguish the two uses (purple "N of M" work vs. gray "Rest").
private struct CountdownPhaseView: View {
    let header: String
    var footer: String = ""
    let tint: Color
    let seconds: Int
    let isActive: Bool
    /// Invoked once the countdown reaches zero (auto-advance to the next page).
    let onFinished: () -> Void

    /// Wall-clock window for the countdown. SwiftUI renders the remaining time from this
    /// range, and the haptics + auto-advance below are derived from `endDate` rather than
    /// a decremented counter so they stay correct even if a tick is delayed.
    @State private var startDate = Date()
    @State private var endDate = Date()
    /// Lowest remaining-second we've already pinged, so a burst of catch-up ticks doesn't
    /// double-buzz.
    @State private var lastPingSecond = Int.max
    /// Guards the auto-advance so it fires exactly once even if ticks pile up.
    @State private var didFinish = false
    private let ticker = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect()

    var body: some View {
        PhaseTimerLayout(header: header, footer: footer, tint: tint) {
            Text(timerInterval: startDate...endDate, countsDown: true)
        }
        .onAppear { if isActive { start() } }
        .onChange(of: isActive) { _, active in if active { start() } }
        .onReceive(ticker) { _ in tick() }
    }

    private func start() {
        startDate = Date()
        endDate = startDate.addingTimeInterval(Double(max(1, seconds)))
        lastPingSecond = Int.max
        didFinish = false
        WorkoutHaptic.start.play()
    }

    private func tick() {
        guard isActive, !didFinish else { return }
        // Round up so the final whole second still pings before we reach zero.
        let remaining = Int(ceil(endDate.timeIntervalSinceNow))

        if remaining <= 0 {
            didFinish = true
            WorkoutHaptic.stop.play()
            onFinished()
        } else if remaining <= 3 && remaining < lastPingSecond {
            // Once-per-second countdown ping for the final three seconds.
            lastPingSecond = remaining
            WorkoutHaptic.tick.play()
        }
    }
}

// MARK: - Finish Phase

/// Terminal page after the last set. **Done** completes the exercise — and fires
/// automatically after a configurable countdown so the user doesn't have to tap with
/// sweaty hands. **One More** appends a bonus set instead.
private struct FinishPhaseView: View {
    let isActive: Bool
    let onDone: () -> Void
    let onOneMore: () -> Void

    @AppStorage("doneCountdownSeconds") private var doneCountdownSeconds: Int = 5

    /// Wall-clock deadline for the auto-Done. `remaining` is what the Done button shows.
    @State private var endDate = Date()
    @State private var remaining = 0
    /// Fires the auto-Done exactly once, and latches off while the page isn't active.
    @State private var didFire = false
    private let ticker = Timer.publish(every: 0.25, on: .main, in: .common).autoconnect()

    var body: some View {
        VStack(spacing: 14) {
            Button(action: fire) {
                Label(remaining > 0 ? "Done · \(remaining)" : "Done", systemImage: "checkmark")
                    .phaseButtonLabel()
            }
            .buttonStyle(.borderedProminent)
            .tint(Color.workTimer)
            .buttonBorderShape(.capsule)

            Button(action: onOneMore) {
                Label("One More", systemImage: "plus")
                    .phaseButtonLabel()
            }
            .buttonStyle(.bordered)
            .buttonBorderShape(.capsule)
        }
        .padding(.horizontal, 40)
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .onAppear { if isActive { start() } }
        .onChange(of: isActive) { _, active in active ? start() : (didFire = true) }
        .onReceive(ticker) { _ in tick() }
    }

    private func start() {
        let total = max(1, doneCountdownSeconds)
        endDate = Date().addingTimeInterval(Double(total))
        remaining = total
        didFire = false
    }

    private func tick() {
        guard isActive, !didFire else { return }
        let r = Int(ceil(endDate.timeIntervalSinceNow))
        remaining = max(0, r)
        if r <= 0 { fire() }
    }

    private func fire() {
        guard !didFire else { return }
        didFire = true
        WorkoutHaptic.success.play()
        onDone()
    }
}
