import Foundation

/// Completion state of a workout or an individual exercise log. Persisted as its
/// raw string in both the JSON documents and the SwiftData cache.
enum WorkoutStatus: String, CaseIterable, Codable, Sendable {
    case notStarted
    case inProgress
    case completed
    case skipped

    var displayName: String {
        switch self {
        case .notStarted: "Not Started"
        case .inProgress: "In Progress"
        case .completed: "Completed"
        case .skipped: "Skipped"
        }
    }

    var name: String { displayName }
}

/// How an exercise's effort is measured. Persisted as its raw `Int` value.
enum LoadType: Int, CaseIterable, Codable, Sendable {
    case none = 0
    case weight = 1
    case duration = 2

    var name: String {
        switch self {
        case .none: "None"
        case .weight: "Weight"
        case .duration: "Duration"
        }
    }
}

/// The kind of training a routine represents — used to tag the watch's Apple Health
/// workout so it lands in the right category (and credits the rings correctly).
/// Persisted as its raw `Int` on `RoutineDocument`; the `HKWorkoutActivityType`
/// mapping lives in `Shared/HealthKit/HealthKitMapping.swift` to keep this enum
/// HealthKit-free.
enum WorkoutActivityType: Int, CaseIterable, Codable, Sendable {
    case traditionalStrength = 0    // default
    case functionalStrength = 1
    case hiit = 2
    case coreTraining = 3
    case cardio = 4
    case cycling = 5
    case warmUp = 6
    case stretching = 7
    case mindAndBody = 8

    var displayName: String {
        switch self {
        case .traditionalStrength: "Strength Training"
        case .functionalStrength: "Functional Strength"
        case .hiit: "HIIT"
        case .coreTraining: "Core Training"
        case .cardio: "Cardio"
        case .cycling: "Cycling"
        case .warmUp: "Warm-Up"
        case .stretching: "Stretching"
        case .mindAndBody: "Mind & Body"
        }
    }

    var systemImage: String {
        switch self {
        case .traditionalStrength: "dumbbell.fill"
        case .functionalStrength: "figure.strengthtraining.functional"
        case .hiit: "figure.highintensity.intervaltraining"
        case .coreTraining: "figure.core.training"
        case .cardio: "figure.run"
        case .cycling: "figure.outdoor.cycle"
        case .warmUp: "figure.arms.open"
        case .stretching: "figure.flexibility"
        case .mindAndBody: "figure.mind.and.body"
        }
    }

    /// Whether a routine of this type can carry a target heart rate. Steady-state
    /// effort calibration ("raise the incline until you hit 140") only makes sense
    /// for the endurance types — a strength set's HR swings by design.
    var supportsHeartRateTarget: Bool {
        switch self {
        case .hiit, .cardio, .cycling: true
        default: false
        }
    }
}

/// Where a live heart-rate reading sits relative to a routine's target rate, with a
/// ±`tolerance` bpm dead band so the indicator doesn't flap around the boundary.
/// Shared by the phone and watch run screens so both classify identically.
enum HeartRateBand: Sendable {
    case low, inRange, high

    static let tolerance = 5

    init(bpm: Double, target: Int) {
        if bpm < Double(target - Self.tolerance) { self = .low }
        else if bpm > Double(target + Self.tolerance) { self = .high }
        else { self = .inRange }
    }
}

/// Where a workout's health metrics came from: real watch sensors, or — for records
/// written by app versions up to 2.x — a phone-side MET estimate made when no watch
/// session ran. The phone no longer writes estimates (recording is watch-only), but
/// `phoneEstimate` is retained so legacy documents on disk still decode. Persisted in
/// `WorkoutMetrics`.
enum MetricSource: String, Codable, Sendable {
    case watch
    case phoneEstimate
}

/// Display unit for weights. The stored weight values are unit-less numbers and
/// are never rewritten when this changes — switching only relabels what's shown.
enum WeightUnit: String, CaseIterable, Codable, Sendable {
    case lb
    case kg

    var displayName: String {
        switch self {
        case .lb: "Pounds (lb)"
        case .kg: "Kilograms (kg)"
        }
    }

    var abbreviation: String {
        switch self {
        case .lb: "lb"
        case .kg: "kg"
        }
    }

    /// Render a stored weight value with the current unit's label, trimming a
    /// trailing ".0" so whole values stay clean: "45 lb", "42.5 kg".
    func format(_ value: Double) -> String {
        let text = value.truncatingRemainder(dividingBy: 1) == 0
            ? String(Int(value))
            : String(value)
        return "\(text) \(abbreviation)"
    }

    /// Render a stored weight value with the current unit's label, e.g. "135 lb".
    func format(_ value: Int) -> String { "\(value) \(abbreviation)" }
}

// MARK: - Goals

/// The predefined practice-goal categories. Deliberately a small, closed set —
/// schedules reference them by raw value, so raw values are stable persisted data.
enum GoalKind: String, CaseIterable, Codable, Sendable, Identifiable {
    // Declaration order IS the initial board order (before any user reorder):
    // the day reads top-to-bottom — morning mobility, then cardio, then strength,
    // with mindfulness closing it out.
    case mobility, cardio, strength, mindfulness

    var id: String { rawValue }

    var displayName: String {
        switch self {
        case .strength: "Strength"
        // Deliberately double-barreled: the same practice serves performance
        // ("endurance") and health ("cardio") motivations — the name excludes neither.
        case .cardio: "Endurance/Cardio"
        case .mobility: "Mobility"
        case .mindfulness: "Mindfulness"
        }
    }

    var systemImage: String {
        switch self {
        case .strength: "dumbbell.fill"
        case .cardio: "figure.run"
        case .mobility: "figure.flexibility"
        case .mindfulness: "brain.head.profile"
        }
    }

    /// A hue name `Color.color(from:)` understands. Four distinct hues so the goals
    /// read apart at a glance.
    var colorName: String {
        switch self {
        case .strength: "red"
        case .cardio: "orange"
        case .mobility: "teal"
        case .mindfulness: "purple"
        }
    }
}

extension GoalKind {
    static let orderDefaultsKey = "goalKindOrder"

    /// All cases in the user's chosen order: stored raw values first (unknown ones
    /// dropped, duplicates collapsed), then any cases missing from the stored list in
    /// declaration order — so newly added kinds appear automatically.
    static func orderedCases(defaults: UserDefaults = .standard) -> [GoalKind] {
        let stored = defaults.stringArray(forKey: orderDefaultsKey) ?? []
        var ordered: [GoalKind] = []
        var seen = Set<GoalKind>()
        for raw in stored {
            if let kind = GoalKind(rawValue: raw), seen.insert(kind).inserted {
                ordered.append(kind)
            }
        }
        for kind in GoalKind.allCases where !seen.contains(kind) {
            ordered.append(kind)
        }
        return ordered
    }

    /// Persist the display order as `[String]` of raw values.
    static func saveOrder(_ kinds: [GoalKind], defaults: UserDefaults = .standard) {
        defaults.set(kinds.map(\.rawValue), forKey: orderDefaultsKey)
    }
}

// MARK: - Schedules

/// How often a schedule expects its routine. Raw values are persisted in ScheduleDocument.
enum ScheduleRecurrence: String, CaseIterable, Codable, Sendable {
    case once, daily, fixedDays

    var displayName: String {
        switch self {
        case .once: "Once"
        case .daily: "Daily"
        case .fixedDays: "Fixed Days"
        }
    }
}

extension ScheduleRecurrence {
    /// Human-readable recurrence description shared by `ScheduleDocument` and the
    /// `Schedule` cache entity, so both render identically. `weekdays` uses Calendar
    /// weekday numbers (1 = Sun … 7 = Sat); `date` is the one-off day (`.once` only).
    /// A degenerate case (fixed days with no weekdays, a nil date) falls back to the
    /// recurrence's own `displayName`.
    func summary(weekdays: [Int]?, date: Date?) -> String {
        switch self {
        case .once:
            return date?.formatDate() ?? displayName   // "Jul 12, 2026" / "Once"
        case .daily:
            return displayName   // "Daily"
        case .fixedDays:
            // A fixed Gregorian calendar's `shortWeekdaySymbols` is 0-indexed from
            // Sunday, so weekday number `n` maps to index `n - 1` regardless of locale.
            let symbols = Calendar(identifier: .gregorian).shortWeekdaySymbols
            let names = (weekdays ?? []).sorted().compactMap { n -> String? in
                (1...7).contains(n) ? symbols[n - 1] : nil
            }
            return names.isEmpty ? displayName : Self.joined(names)
        }
    }

    /// Join weekday short names conversationally: "Mon", "Mon & Thu", "Mon, Wed & Fri".
    private static func joined(_ names: [String]) -> String {
        switch names.count {
        case 0: return ""
        case 1: return names[0]
        case 2: return "\(names[0]) & \(names[1])"
        default: return names.dropLast().joined(separator: ", ") + " & " + (names.last ?? "")
        }
    }
}
