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

import SwiftUI
import WatchKit

/// Runs a single exercise as a horizontally-paged HIIT cycle:
///
///   Work₁ → Rest₁ → Work₂ → Rest₂ → … → Rest₍ₙ₋₁₎ → Workₙ
///
/// The **work** phase counts *up* (a stopwatch for the current set); the user swipes
/// left when they're done. The **rest** phase counts *down* from the configurable rest
/// time, beeps once per second in the final three seconds, and then auto-advances to
/// the next work phase. The final work phase has no rest after it — instead it offers
/// **One More** (append a bonus set and keep going) and **Done** (mark the exercise
/// complete and return to the list).
struct ExerciseProgressView: View {
    @Environment(\.dismiss) private var dismiss

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

    /// 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 showingCancelConfirm = false
    @State private var didRestorePage = false

    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)
        // Resume on the first unfinished set's work page (clamped to the last set).
        let completed = min(max(0, log?.currentStateIndex ?? 0), sets - 1)
        _currentPage = State(initialValue: completed * 2)
    }

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

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

    /// The first unfinished set's work page (clamped to the last set). Resuming an
    /// exercise opens here, skipping any completed work/rest pairs.
    private var resumePage: Int {
        let completed = min(max(0, log?.currentStateIndex ?? 0), setCount - 1)
        return completed * 2
    }

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

    var body: some View {
        TabView(selection: $currentPage) {
            ForEach(0..<totalPages, id: \.self) { index in
                page(for: index)
                    .tag(index)
            }
        }
        .tabViewStyle(.page)
        .toolbar {
            ToolbarItem(placement: .cancellationAction) {
                Button {
                    showingCancelConfirm = true
                } label: {
                    Image(systemName: "xmark")
                }
            }
        }
        .confirmationDialog("Cancel Exercise?", isPresented: $showingCancelConfirm, titleVisibility: .visible) {
            Button("Cancel Exercise", role: .destructive) { dismiss() }
            Button("Continue", role: .cancel) { }
        }
        .onChange(of: currentPage) { _, newPage in
            recordProgress(for: newPage)
        }
        .onAppear {
            // 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.
            guard !didRestorePage else { return }
            didRestorePage = true
            jumpToResumePage()
            Task { @MainActor in jumpToResumePage() }
        }
    }

    /// 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 index.isMultiple(of: 2) {
            // Work phase. The last page (set N) finishes the exercise.
            WorkPhaseView(
                setNumber: index / 2 + 1,
                totalSets: setCount,
                detail: detail,
                isLast: index == totalPages - 1,
                isActive: isActive,
                onOneMore: addSet,
                onDone: { completeExercise(); dismiss() }
            )
        } else {
            // Rest phase. Auto-advances to the next work page when the timer hits zero.
            RestPhaseView(isActive: isActive) {
                withAnimation { advance(from: index) }
            }
        }
    }

    // MARK: - Mutations

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

    /// Append a bonus set: grow the plan, record the just-finished set as done, and
    /// slide into the rest period that now follows the previously-final work page.
    private func addSet() {
        let restPage = currentPage + 1   // the rest page that becomes valid after the bump
        setCount += 1

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

        withAnimation { currentPage = restPage }
    }

    /// Page index → completed-set count: Work₁(0)→0, Rest₁(1)→1, Work₂(2)→1, … —
    /// i.e. `(pageIndex + 1) / 2`. Reaching set N as *completed* only happens via Done.
    ///
    /// Progress is **monotonic**: completing a work phase advances the count (and
    /// forwards it to the phone), but swiping back — or a transient TabView snap to
    /// page 0 — never un-counts a finished set.
    private func recordProgress(for pageIndex: Int) {
        let reached = min((pageIndex + 1) / 2, setCount)

        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
        if reached >= setCount {
            doc.logs[i].status = WorkoutStatus.completed.rawValue
            doc.logs[i].completed = true
        } else {
            doc.logs[i].status = WorkoutStatus.inProgress.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: - Work Phase

private struct WorkPhaseView: View {
    let setNumber: Int
    let totalSets: Int
    let detail: String
    let isLast: Bool
    let isActive: Bool
    let onOneMore: () -> Void
    let onDone: () -> Void

    @State private var elapsed = 0
    private let ticker = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect()

    var body: some View {
        VStack(spacing: 6) {
            Text("Set \(setNumber) of \(totalSets)")
                .font(.headline)
                .foregroundStyle(.secondary)

            Text(clockString(elapsed))
                .font(.system(size: 48, weight: .bold, design: .rounded))
                .monospacedDigit()
                .foregroundStyle(.green)

            Text(detail)
                .font(.subheadline)
                .foregroundStyle(.secondary)

            if isLast {
                VStack(spacing: 6) {
                    Button(action: onOneMore) {
                        Label("One More", systemImage: "plus")
                            .frame(maxWidth: .infinity)
                    }
                    .tint(.blue)

                    Button(action: onDone) {
                        Label("Done", systemImage: "checkmark")
                            .frame(maxWidth: .infinity)
                    }
                    .tint(.green)
                }
                .buttonStyle(.borderedProminent)
                .padding(.top, 4)
            } else {
                Label("Swipe to rest", systemImage: "chevron.right")
                    .font(.caption2)
                    .foregroundStyle(.secondary)
            }
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .onAppear { if isActive { restart() } }
        .onChange(of: isActive) { _, active in if active { restart() } }
        .onReceive(ticker) { _ in if isActive { elapsed += 1 } }
    }

    private func restart() {
        elapsed = 0
        WKInterfaceDevice.current().play(.start)
    }
}

// MARK: - Rest Phase

private struct RestPhaseView: View {
    let isActive: Bool
    /// Invoked once the countdown reaches zero (auto-advance to the next work phase).
    let onFinished: () -> Void

    @AppStorage("restSeconds") private var restSeconds: Int = 45
    @State private var remaining = 0
    private let ticker = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect()

    var body: some View {
        VStack(spacing: 6) {
            Text("Rest")
                .font(.headline)
                .foregroundStyle(.secondary)

            Text(clockString(max(0, remaining)))
                .font(.system(size: 54, weight: .bold, design: .rounded))
                .monospacedDigit()
                .foregroundStyle(.orange)

            Label("Swipe to skip", systemImage: "chevron.right")
                .font(.caption2)
                .foregroundStyle(.secondary)
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .onAppear { if isActive { start() } }
        .onChange(of: isActive) { _, active in if active { start() } }
        .onReceive(ticker) { _ in tick() }
    }

    private func start() {
        remaining = max(1, restSeconds)
        WKInterfaceDevice.current().play(.start)
    }

    private func tick() {
        guard isActive, remaining > 0 else { return }
        remaining -= 1

        if remaining == 0 {
            // Time's up — final cue and slide to the next work phase.
            WKInterfaceDevice.current().play(.stop)
            onFinished()
        } else if remaining <= 3 {
            // Once-per-second countdown ping for the final three seconds.
            WKInterfaceDevice.current().play(.notification)
        }
    }
}

// MARK: - Shared

private func clockString(_ seconds: Int) -> String {
    String(format: "%d:%02d", seconds / 60, seconds % 60)
}
