import IndieSync
import Foundation
import SwiftData

// SwiftData cache entities. These are a rebuildable read-through cache of the
// iCloud Drive JSON documents — never the source of truth. App code reads them
// (via @Query); all writes go through `SyncEngine` to the document files, and the
// metadata observer mirrors the files back into these entities (see Mappers).
//
// `id` is the aggregate/child ULID (stable across cache rebuilds, unlike the
// SwiftData PersistentIdentifier). Computed helpers preserve the API the views
// used against the old Core Data classes.

extension PersistentModel {
    /// True while this model is registered with a live context. Reading a persisted
    /// property on a dead model traps, and any entity can die under a view that
    /// retains it (remote delete, reconcile prune — the cache is rebuildable). Check
    /// this before every such read: `isDeleted` alone misses the unregistered state
    /// after the deletion saves, when `isDeleted` reads false again but reads still
    /// trap (see `WorkoutDocument.init?(fromLive:)`).
    var isLive: Bool { !isDeleted && modelContext != nil }
}

// MARK: - Routine

@Model
final class Routine {
    @Attribute(.unique) var id: String = ULID.make()
    var name: String = ""
    var color: String = "indigo"
    var systemImage: String = "dumbbell.fill"
    var order: Int = 0
    var createdAt: Date = Date()
    var updatedAt: Date = Date()
    var jsonRelativePath: String = ""
    var activityTypeRaw: Int = 0
    var restSeconds: Int?
    var autoAdvance: Bool?
    var targetHeartRate: Int?

    @Relationship(deleteRule: .cascade, inverse: \Exercise.routine)
    var exercises: [Exercise] = []

    init(id: String, name: String, color: String, systemImage: String, order: Int,
         createdAt: Date, updatedAt: Date, jsonRelativePath: String) {
        self.id = id
        self.name = name
        self.color = color
        self.systemImage = systemImage
        self.order = order
        self.createdAt = createdAt
        self.updatedAt = updatedAt
        self.jsonRelativePath = jsonRelativePath
    }

    static let unnamed = "Unnamed Routine"

    var exercisesArray: [Exercise] { exercises.sorted { $0.order < $1.order } }

    var activityTypeEnum: WorkoutActivityType {
        get { WorkoutActivityType(rawValue: activityTypeRaw) ?? .traditionalStrength }
        set { activityTypeRaw = newValue.rawValue }
    }
}

// MARK: - Exercise

@Model
final class Exercise {
    @Attribute(.unique) var id: String = ULID.make()
    var name: String = ""
    var order: Int = 0
    var sets: Int = 0
    var reps: Int = 0
    var weight: Double = 0
    var loadType: Int = LoadType.weight.rawValue
    var durationTotalSeconds: Int = 0
    // Machine comfort settings — stored directly as an optional Codable array
    // (SwiftData persists the `[MachineSetting]` composite), mirroring how
    // `Workout` stores `hrZoneSeconds` as a plain `[Double]?`. `nil` → not a
    // machine exercise; empty → a machine exercise with nothing recorded yet.
    var machineSettings: [MachineSetting]?
    // The library exercise `name` was customized from; nil → `name` IS the
    // library name. Mirrors `ExerciseDocument.libraryName`.
    var libraryName: String?

    var routine: Routine?

    init(id: String, name: String, order: Int, sets: Int, reps: Int, weight: Double,
         loadType: Int, durationTotalSeconds: Int, machineSettings: [MachineSetting]? = nil,
         libraryName: String? = nil) {
        self.id = id
        self.name = name
        self.order = order
        self.sets = sets
        self.reps = reps
        self.weight = weight
        self.loadType = loadType
        self.durationTotalSeconds = durationTotalSeconds
        self.machineSettings = machineSettings
        self.libraryName = libraryName
    }

    var loadTypeEnum: LoadType {
        get { LoadType(rawValue: loadType) ?? .weight }
        set { loadType = newValue.rawValue }
    }

    /// The name to resolve figure/info/cues against — see `ExerciseDocument.libraryExerciseName`.
    var libraryExerciseName: String { libraryName ?? name }

    /// One-line plan summary for list rows — duration exercises show their hold time
    /// ("3 × 45 sec"), unloaded ones drop the weight ("3 × 12 reps"), weighted ones
    /// keep the full "3 × 10 × 40 lb".
    func planSummary(weightUnit: WeightUnit) -> String {
        switch loadTypeEnum {
        case .duration:
            let m = durationMinutes, s = durationSeconds
            let hold = m > 0 && s > 0 ? "\(m)m \(s)s" : (m > 0 ? "\(m) min" : "\(s) sec")
            return "\(sets) × \(hold)"
        case .none:
            return "\(sets) × \(reps) reps"
        case .weight:
            return "\(sets) × \(reps) × \(weightUnit.format(weight))"
        }
    }

    /// Minutes component of the total duration (for min:sec pickers).
    var durationMinutes: Int {
        get { durationTotalSeconds / 60 }
        set { durationTotalSeconds = newValue * 60 + durationSeconds }
    }

    /// Seconds component (0–59) of the total duration.
    var durationSeconds: Int {
        get { durationTotalSeconds % 60 }
        set { durationTotalSeconds = durationMinutes * 60 + newValue }
    }
}

// MARK: - Workout

@Model
final class Workout {
    @Attribute(.unique) var id: String = ULID.make()
    var routineID: String?
    var routineName: String?
    var start: Date = Date()
    var end: Date?
    var statusRaw: String = WorkoutStatus.notStarted.rawValue
    var createdAt: Date = Date()
    var updatedAt: Date = Date()
    var jsonRelativePath: String = ""

    // Health metrics — flat columns mirroring `WorkoutMetrics` (SwiftData @Model
    // can't store the nested Codable cleanly). Assemble via the `metrics` computed
    // property. All nil until a workout finishes and a writer fills them in.
    var metricActiveEnergyKcal: Double?
    var metricAvgHeartRate: Double?
    var metricMaxHeartRate: Double?
    var metricMinHeartRate: Double?
    var metricTotalVolume: Double?
    var metricHRZoneSeconds: [Double]?
    var metricHealthKitWorkoutUUID: String?
    var metricSourceRaw: String?
    var metricRecordedAt: Date?

    /// Mirrors the document's per-log deletion tombstones (`logID → when deleted`), so the
    /// phone re-derives them into any `WorkoutDocument` it rebuilds and the merge stays
    /// deletion-safe across cache round-trips. Phone-authored; see `WorkoutMergePlanner`.
    var deletedLogIDs: [String: Date]?
    var restSeconds: Int?
    var autoAdvance: Bool?
    var targetHeartRate: Int?

    @Relationship(deleteRule: .cascade, inverse: \WorkoutLog.workout)
    var logs: [WorkoutLog] = []

    init(id: String, routineID: String?, routineName: String?, start: Date, end: Date?,
         statusRaw: String, createdAt: Date, updatedAt: Date, jsonRelativePath: String) {
        self.id = id
        self.routineID = routineID
        self.routineName = routineName
        self.start = start
        self.end = end
        self.statusRaw = statusRaw
        self.createdAt = createdAt
        self.updatedAt = updatedAt
        self.jsonRelativePath = jsonRelativePath
    }

    var status: WorkoutStatus {
        get { WorkoutStatus(rawValue: statusRaw) ?? .notStarted }
        set { statusRaw = newValue.rawValue }
    }

    /// True while this workout is an untouched draft: created by a start tap, but no
    /// exercise has ever left `.notStarted`. Peeking into a routine must leave no
    /// trace, so the start paths discard a pristine draft when the user backs out of
    /// it instead of persisting a phantom "in progress" workout.
    var isPristineDraft: Bool {
        status == .notStarted && logs.allSatisfy { $0.status == .notStarted }
    }

    var statusName: String { status.displayName }

    var logsArray: [WorkoutLog] { logs.sorted { $0.order < $1.order } }

    var label: String {
        if status == .completed, let endDate = end {
            if start.isSameDay(as: endDate) {
                return "\(start.formattedDate())—\(endDate.formattedTime())"
            } else {
                return "\(start.formattedDate())—\(endDate.formattedDate())"
            }
        } else {
            return start.formattedDate()
        }
    }

    /// Assembles the flat `metric*` columns into a `WorkoutMetrics`, or nil when none
    /// have been recorded. `source`/`recordedAt` are always set together when metrics
    /// exist, so they gate presence.
    var metrics: WorkoutMetrics? {
        get {
            guard let sourceRaw = metricSourceRaw,
                  let source = MetricSource(rawValue: sourceRaw),
                  let recordedAt = metricRecordedAt else { return nil }
            return WorkoutMetrics(
                activeEnergyKcal: metricActiveEnergyKcal,
                avgHeartRate: metricAvgHeartRate,
                maxHeartRate: metricMaxHeartRate,
                minHeartRate: metricMinHeartRate,
                totalVolume: metricTotalVolume,
                hrZoneSeconds: metricHRZoneSeconds,
                healthKitWorkoutUUID: metricHealthKitWorkoutUUID,
                source: source,
                recordedAt: recordedAt)
        }
        set {
            metricActiveEnergyKcal = newValue?.activeEnergyKcal
            metricAvgHeartRate = newValue?.avgHeartRate
            metricMaxHeartRate = newValue?.maxHeartRate
            metricMinHeartRate = newValue?.minHeartRate
            metricTotalVolume = newValue?.totalVolume
            metricHRZoneSeconds = newValue?.hrZoneSeconds
            metricHealthKitWorkoutUUID = newValue?.healthKitWorkoutUUID
            metricSourceRaw = newValue?.source.rawValue
            metricRecordedAt = newValue?.recordedAt
        }
    }
}

// MARK: - WorkoutLog

@Model
final class WorkoutLog {
    @Attribute(.unique) var id: String = ULID.make()
    var exerciseName: String = ""
    var order: Int = 0
    var sets: Int = 0
    var reps: Int = 0
    var weight: Double = 0
    var loadType: Int = LoadType.weight.rawValue
    var durationTotalSeconds: Int = 0
    var currentStateIndex: Int = 0
    var statusRaw: String = WorkoutStatus.notStarted.rawValue
    var notes: String?
    var date: Date = Date()
    // Snapshot of the exercise's machine comfort settings at plan time — stored
    // directly as an optional Codable array, same pattern as `Exercise`.
    var machineSettings: [MachineSetting]?
    var startedAt: Date?
    var completedAt: Date?
    // Per-set actuals — stored directly as an optional Codable array, same
    // pattern as `machineSettings`. `nil` → legacy file / nothing recorded.
    var setEntries: [SetEntry]?
    // Mirrors the document's per-log `updatedAt` (named to dodge a future
    // SwiftData column collision). Reserved for H1's per-log merge; unused.
    var logUpdatedAt: Date?
    // Plan-time snapshot of the exercise's `libraryName`. Mirrors
    // `WorkoutLogDocument.libraryName`.
    var libraryName: String?

    var workout: Workout?

    init(id: String, exerciseName: String, order: Int, sets: Int, reps: Int, weight: Double,
         loadType: Int, durationTotalSeconds: Int, currentStateIndex: Int,
         statusRaw: String, notes: String?, date: Date,
         machineSettings: [MachineSetting]? = nil,
         startedAt: Date? = nil, completedAt: Date? = nil,
         setEntries: [SetEntry]? = nil, logUpdatedAt: Date? = nil,
         libraryName: String? = nil) {
        self.id = id
        self.exerciseName = exerciseName
        self.order = order
        self.sets = sets
        self.reps = reps
        self.weight = weight
        self.loadType = loadType
        self.durationTotalSeconds = durationTotalSeconds
        self.currentStateIndex = currentStateIndex
        self.statusRaw = statusRaw
        self.notes = notes
        self.date = date
        self.machineSettings = machineSettings
        self.startedAt = startedAt
        self.completedAt = completedAt
        self.setEntries = setEntries
        self.logUpdatedAt = logUpdatedAt
        self.libraryName = libraryName
    }

    var status: WorkoutStatus {
        get { WorkoutStatus(rawValue: statusRaw) ?? .notStarted }
        set { statusRaw = newValue.rawValue }
    }

    var loadTypeEnum: LoadType {
        get { LoadType(rawValue: loadType) ?? .weight }
        set { loadType = newValue.rawValue }
    }

    /// The name to resolve figure/info/cues against — see `ExerciseDocument.libraryExerciseName`.
    var libraryExerciseName: String { libraryName ?? exerciseName }

    var durationMinutes: Int {
        get { durationTotalSeconds / 60 }
        set { durationTotalSeconds = newValue * 60 + durationSeconds }
    }

    var durationSeconds: Int {
        get { durationTotalSeconds % 60 }
        set { durationTotalSeconds = durationMinutes * 60 + newValue }
    }
}

// MARK: - Schedule

@Model
final class Schedule {
    @Attribute(.unique) var id: String = ULID.make()
    var routineID: String = ""
    var routineName: String = ""
    var goalRaw: String?
    var recurrenceRaw: String = ScheduleRecurrence.daily.rawValue
    var weekdays: [Int]?
    var date: Date?
    /// Reminder time as minutes from midnight (local wall-clock); nil = no reminder.
    var reminderMinutes: Int?
    var order: Int = 0
    var createdAt: Date = Date()
    var updatedAt: Date = Date()
    var jsonRelativePath: String = ""

    init(id: String, routineID: String, routineName: String, goalRaw: String?,
         recurrenceRaw: String, weekdays: [Int]?, date: Date? = nil, reminderMinutes: Int? = nil,
         order: Int, createdAt: Date, updatedAt: Date, jsonRelativePath: String) {
        self.id = id
        self.routineID = routineID
        self.routineName = routineName
        self.goalRaw = goalRaw
        self.recurrenceRaw = recurrenceRaw
        self.weekdays = weekdays
        self.date = date
        self.reminderMinutes = reminderMinutes
        self.order = order
        self.createdAt = createdAt
        self.updatedAt = updatedAt
        self.jsonRelativePath = jsonRelativePath
    }

    var goalKind: GoalKind? {
        get { goalRaw.flatMap(GoalKind.init(rawValue:)) }
        set { goalRaw = newValue?.rawValue }
    }

    var recurrenceEnum: ScheduleRecurrence {
        get { ScheduleRecurrence(rawValue: recurrenceRaw) ?? .daily }
        set { recurrenceRaw = newValue.rawValue }
    }

    /// Human-readable recurrence line — shares its formatting with `ScheduleDocument`
    /// via `ScheduleRecurrence.summary(weekdays:date:)`.
    var recurrenceSummary: String {
        recurrenceEnum.summary(weekdays: weekdays, date: date)
    }
}
