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

import SwiftUI

/// Thin coordinator that lets a single pushed run walk across exercises in a flow-mode
/// routine (`autoAdvance`). It owns which log is on screen and swaps it when the running
/// `ExerciseProgressView` finishes an exercise; `.id(currentLogID)` rebuilds the child
/// fresh for each exercise, so the delicate per-exercise run/mirror machinery stays
/// exactly as it is — this only automates the "open the next exercise" step.
///
/// For a non-flow routine nothing ever calls `advance`, so this is transparent: it presents
/// one exercise and behaves identically to hosting `ExerciseProgressView` directly.
struct RunFlowView: View {
    @Environment(LiveRunState.self) private var liveRun

    @Binding var doc: WorkoutDocument
    let startLogID: String
    let onChange: () -> Void
    let onLive: (LiveProgress) -> Void
    /// Send the "stop mirroring this run" signal for a given log. Parameterized by log id
    /// because the run's current log advances; the leaving exercise's id is what ends.
    let sendLiveEnded: (String) -> Void
    let onActivity: (LiveProgress) -> Void
    let onActivityEnded: () -> Void
    var speechAnnouncer: SpeechAnnouncer? = nil

    @State private var currentLogID: String
    /// True once we've auto-advanced at least once — the current exercise then skips its
    /// Ready page and begins on appear. The first exercise (manual entry) keeps Ready.
    @State private var enteredViaFlow = false
    /// Set across an exercise hand-off so the leaving exercise's teardown doesn't tear the
    /// mirror / Live Activity down — the run is continuing, not ending.
    @State private var advancing = false

    init(doc: Binding<WorkoutDocument>, startLogID: String, onChange: @escaping () -> Void,
         onLive: @escaping (LiveProgress) -> Void, sendLiveEnded: @escaping (String) -> Void,
         onActivity: @escaping (LiveProgress) -> Void, onActivityEnded: @escaping () -> Void,
         speechAnnouncer: SpeechAnnouncer? = nil) {
        self._doc = doc
        self.startLogID = startLogID
        self.onChange = onChange
        self.onLive = onLive
        self.sendLiveEnded = sendLiveEnded
        self.onActivity = onActivity
        self.onActivityEnded = onActivityEnded
        self.speechAnnouncer = speechAnnouncer
        _currentLogID = State(initialValue: startLogID)
    }

    var body: some View {
        // The ZStack is a stable identity (its lifecycle fires once for the whole run),
        // while the child re-identifies per exercise via `.id(currentLogID)`.
        ZStack {
            ExerciseProgressView(
                doc: $doc,
                logID: currentLogID,
                onChange: onChange,
                onLive: onLive,
                onLiveEnded: { if !advancing { sendLiveEnded(currentLogID) } },
                onActivity: onActivity,
                onActivityEnded: { if !advancing { onActivityEnded() } },
                incomingFrame: liveRun.current.flatMap { $0.logID == currentLogID ? $0 : nil },
                speechAnnouncer: speechAnnouncer,
                enteredViaFlow: enteredViaFlow,
                onAdvance: advance(to:)
            )
            .id(currentLogID)
        }
        .onAppear { liveRun.navigatedRunID = currentLogID }
        .onDisappear {
            if liveRun.navigatedRunID == currentLogID { liveRun.navigatedRunID = nil }
            // Silence narration only when the *whole run* leaves. This host stays mounted
            // across per-exercise hand-offs (the child re-ids), so stopping here — rather
            // than in the child's onDisappear — no longer cuts off the next exercise's cue.
            speechAnnouncer?.stop()
        }
        .onChange(of: liveRun.current) { _, frame in
            followRemoteHandoff(frame)
        }
    }

    /// Follow the driver across an exercise boundary. Auto-advances are never broadcast, so
    /// a frame for a *different* log of this flow workout is always the peer's discrete
    /// hand-off into the next exercise — which the per-exercise child can't follow (its
    /// incoming-frame filter is scoped to `currentLogID`). Forward-only by plan order and
    /// only into a still-runnable log, so a late-delivered frame for an earlier exercise
    /// can never yank the flow backwards.
    private func followRemoteHandoff(_ frame: LiveProgress?) {
        guard doc.autoAdvance == true,
              let frame, frame.workoutID == doc.id, frame.logID != currentLogID else { return }
        let sorted = doc.logs.sorted { $0.order != $1.order ? $0.order < $1.order : $0.id < $1.id }
        guard let from = sorted.firstIndex(where: { $0.id == currentLogID }),
              let to = sorted.firstIndex(where: { $0.id == frame.logID }), to > from else { return }
        let status = WorkoutStatus(rawValue: sorted[to].status) ?? .notStarted
        guard status == .notStarted || status == .inProgress else { return }
        advance(to: frame.logID)
    }

    /// Hand off to the next exercise. Suppresses the leaving exercise's live-ended/activity
    /// teardown so the mirror follows across the boundary, moves `navigatedRunID` forward so
    /// this device doesn't mirror its own next exercise, then swaps the presented log.
    private func advance(to next: String) {
        advancing = true
        liveRun.navigatedRunID = next
        enteredViaFlow = true
        currentLogID = next
        // Release the guard after the swap settles (old view torn down, new one begun).
        Task { @MainActor in advancing = false }
    }
}
