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

import ActivityKit
import Foundation
import os

/// Owns the lifecycle of the active-workout Live Activity (lock screen + Dynamic Island).
///
/// It is driven by the **same** run-flow frames the phone↔watch mirror uses (`LiveProgress`):
/// `ExerciseProgressView` hands us one on every phase / set settle, we translate it to a
/// `WorkoutActivityAttributes.ContentState`, and start or update the activity. Because the
/// content carries wall-clock anchors, the widget's countdowns keep ticking while the phone is
/// locked without any push update from here.
///
/// Lifecycle:
///   • **Start** — the first non-Ready frame of a run spins the activity up.
///   • **Update** — every subsequent frame refreshes it (idempotent).
///   • **End** — leaving the run flow tears it down; a completed run lingers briefly on a
///     terminal "Complete" state before auto-dismissing.
///
/// Degrades silently when Live Activities are disabled (`areActivitiesEnabled`).
///
/// Concurrency: the public entry points are **synchronous**, so `currentLogID` / `lastState`
/// (both `Sendable`) mutate in call order on the main actor. The async ActivityKit I/O is fired
/// off to detached tasks that resolve the live activity from the `nonisolated`
/// `Activity.activities` static — a *disconnected* value that crosses into the task cleanly,
/// unlike a non-`Sendable` `Activity` stashed in a main-actor-isolated property. The run flow is
/// per-exercise, so there's only ever one of our activities live at a time.
@MainActor
final class WorkoutLiveActivityController {
    private static let log = Logger(subsystem: "dev.rzen.indie.Workouts", category: "live-activity")

    private typealias State = WorkoutActivityAttributes.ContentState
    private typealias Content = ActivityContent<State>

    /// The run the live activity belongs to, so a frame for a *different* run replaces it, and a
    /// `nil` means nothing is showing.
    private var currentLogID: String?
    /// The most recent content state, so `end()` can render a terminal "Complete" card without
    /// reaching back into the (non-`Sendable`) activity handle.
    private var lastState: State?

    private var isEnabled: Bool { ActivityAuthorizationInfo().areActivitiesEnabled }

    // MARK: - Update

    /// Reflect the given run frame on the Live Activity — starting one if needed. Fired on every
    /// settle (human swipe, auto-advance, or a followed watch frame), so it must be idempotent.
    func update(_ frame: LiveProgress) {
        guard isEnabled else { return }

        // A frame for a run other than the one we're showing — drop the stale activity first.
        if let current = currentLogID, current != frame.logID {
            reset()
        }

        // Swiping back to the Ready page resets the exercise — nothing to show anymore.
        guard frame.phase != .ready else {
            if currentLogID != nil { reset() }
            return
        }

        let state = Self.contentState(from: frame)
        lastState = state
        let content = Content(state: state, staleDate: nil)

        if currentLogID == nil {
            // Don't spin one up just for the Ready lead-in; this is the first real frame.
            currentLogID = frame.logID
            start(content)
        } else {
            pushUpdate(content)
        }
    }

    private func start(_ content: Content) {
        do {
            _ = try Activity.request(
                attributes: WorkoutActivityAttributes(),
                content: content,
                pushType: nil
            )
        } catch {
            Self.log.error("Live Activity start failed: \(error, privacy: .public)")
            currentLogID = nil
            lastState = nil
        }
    }

    // MARK: - End

    /// Leave the run flow. If the run reached its terminal Done state, linger briefly on a
    /// "Complete" card before dismissing; otherwise dismiss at once.
    func end() {
        guard let state = lastState else { return }
        currentLogID = nil
        lastState = nil

        if state.phase == .done {
            var terminal = state
            terminal.phaseEnd = nil   // stop the auto-Done countdown; show the finished state
            endAll(final: Content(state: terminal, staleDate: nil),
                   dismissal: .after(Date().addingTimeInterval(4)))
        } else {
            endAll(final: nil, dismissal: .immediate)
        }
    }

    /// Immediately tear down whatever is showing (a run switch or a Ready reset).
    private func reset() {
        currentLogID = nil
        lastState = nil
        endAll(final: nil, dismissal: .immediate)
    }

    /// Dismiss any activity still on screen from a previous launch (e.g. the app was killed
    /// mid-run). Called defensively at bootstrap.
    func endStaleActivities() async {
        currentLogID = nil
        lastState = nil
        for stale in Activity<WorkoutActivityAttributes>.activities {
            await stale.end(nil, dismissalPolicy: .immediate)
        }
    }

    // MARK: - Fire-and-forget ActivityKit I/O

    /// Push a new content state to the live activity. `content` is `Sendable`; the activity is
    /// resolved inside the task from the nonisolated `activities` list, so nothing non-`Sendable`
    /// crosses the main-actor boundary.
    private nonisolated func pushUpdate(_ content: Content) {
        Task {
            for activity in Activity<WorkoutActivityAttributes>.activities {
                await activity.update(content)
            }
        }
    }

    /// End every live activity of our type, optionally showing a terminal `final` state first.
    private nonisolated func endAll(final content: Content?, dismissal: ActivityUIDismissalPolicy) {
        Task {
            for activity in Activity<WorkoutActivityAttributes>.activities {
                await activity.end(content, dismissalPolicy: dismissal)
            }
        }
    }

    // MARK: - Helpers

    /// Translate a run-flow frame into a widget content state (mapping `.finish` → `.done`).
    private static func contentState(from frame: LiveProgress) -> State {
        let phase: State.Phase
        switch frame.phase {
        case .ready:  phase = .ready
        case .work:   phase = .work
        case .rest:   phase = .rest
        case .finish: phase = .done
        }
        return State(
            exerciseName: frame.exerciseName,
            phase: phase,
            setIndex: frame.setIndex,
            setCount: frame.setCount,
            detail: frame.detail,
            phaseStart: frame.phaseStart,
            phaseEnd: frame.phaseEnd
        )
    }
}
