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

import Foundation
import Observation

/// Phone-side holder for the *ephemeral* live-run frames the watch broadcasts while it drives
/// an exercise (see `LiveProgress`). The mirror UI observes this; nothing here is persisted.
///
/// Phase 1 is watch-drives / phone-mirrors, so this is read-only state fed by the connectivity
/// bridge — the phone never sends back, which is why there's no echo loop to guard against yet.
@Observable
@MainActor
final class LiveRunState {
    /// The latest frame from the driving device, or `nil` when no run is being mirrored.
    private(set) var current: LiveProgress?

    /// A log the user manually closed the mirror for; suppressed until that run ends.
    private var mutedLogID: String?

    /// The run currently open in a non-cover driver on *this* device (its in-list/navigated
    /// `ExerciseProgressView`). When an incoming frame is for that run, the device follows it
    /// inline in that driver, so the mirror cover doesn't stack on top of a run already open.
    var navigatedRunID: String?

    /// The frame to actually present as a cover: the latest frame, unless the user dismissed it
    /// or already has that run open inline.
    var presentable: LiveProgress? {
        guard let c = current, c.logID != mutedLogID, c.logID != navigatedRunID else { return nil }
        return c
    }

    /// Latest live heart-rate sample forwarded by the watch (bpm), or `nil` when none is
    /// fresh. Device-level, not run-scoped — it's the wearer's pulse. Auto-clears after
    /// `hrStaleAfter` without a new sample, so the UI never shows a frozen number after
    /// the watch stops sending (session ended, unreachable).
    private(set) var heartRate: Double?

    /// Running active-calorie total for the watch session, riding along with each HR
    /// sample. Nil until the first energy sample (or from an older watch build).
    private(set) var activeEnergyKcal: Double?

    /// HR zone (1–5) the current rate falls in, computed on the watch (the only place
    /// that knows max HR). Nil when the wearer's age is unknown.
    private(set) var hrZone: Int?

    private var heartRateExpiry: Task<Void, Never>?
    private static let hrStaleAfter: Duration = .seconds(30)

    /// Apply an incoming live sample and (re)arm the shared staleness expiry — the
    /// three values arrive together, so they go stale together.
    func applyLiveSample(bpm: Double, kcal: Double?, zone: Int?) {
        heartRate = bpm
        activeEnergyKcal = kcal
        hrZone = zone
        heartRateExpiry?.cancel()
        heartRateExpiry = Task { [weak self] in
            try? await Task.sleep(for: Self.hrStaleAfter)
            guard !Task.isCancelled else { return }
            self?.heartRate = nil
            self?.activeEnergyKcal = nil
            self?.hrZone = nil
        }
    }

    /// Apply an incoming frame, dropping a stale (or redelivered) one for the same run.
    func apply(_ frame: LiveProgress) {
        if let c = current, c.logID == frame.logID, !frame.isNewer(than: c) { return }
        current = frame
    }

    /// The driver left the run (cancel / done / navigated away) — stop mirroring it.
    func end(logID: String) {
        if current?.logID == logID { current = nil }
        if mutedLogID == logID { mutedLogID = nil }
    }

    /// The user dismissed the mirror; don't re-present this run until it ends.
    func mute() {
        mutedLogID = current?.logID
    }
}
