import IndieSync
import Foundation

/// Wire format for the iPhone↔Watch bridge. The phone is the only device that
/// touches iCloud Drive; the watch round-trips domain documents through the phone.
/// Payloads carry the shared `*Document` types (JSON-encoded `Data` blobs, which
/// WatchConnectivity allows) keyed by stable ULIDs — no name/date reconciliation.
enum WCPayload {
    static let typeKey = "type"
    // PINNED VALUES: the constant names became `routinesKey` / `editingRoutineIDKey`,
    // but their string VALUES stay `"splits"` / `"editingSplitID"` so an updated phone
    // still interops with a not-yet-updated watch over WatchConnectivity.
    static let routinesKey = "splits"
    static let workoutsKey = "workouts"
    static let workoutKey = "workout"
    static let restSecondsKey = "restSeconds"
    static let doneCountdownSecondsKey = "doneCountdownSeconds"
    static let weightUnitKey = "weightUnit"
    static let editingWorkoutIDKey = "editingWorkoutID"
    static let editingRoutineIDKey = "editingSplitID"

    static let workoutUpdateType = "workoutUpdate"   // watch → phone (one workout)
    static let requestSyncType = "requestSync"       // watch → phone (please push state)
    static let settingsUpdateType = "settingsUpdate" // watch → phone (a setting changed on the watch)
    static let liveProgressType = "liveProgress"     // watch → phone (ephemeral mirror frame)
    static let liveEndedType = "liveEnded"           // watch → phone (stop mirroring a run)
    static let liveHeartRateType = "liveHeartRate"   // watch → phone (ephemeral HR sample)

    // MARK: - Phone → Watch (application context: latest-state-wins)

    /// `editingWorkoutID` / `editingRoutineID` are an exclusive-edit lock: while the phone
    /// has a workout's exercise (or a routine) open in an editor, the watch parks any
    /// matching run and locks re-entry, so only one device owns the run at a time. They're
    /// part of the same latest-wins context — absent keys mean "not editing" (lock clear).
    static func encodeState(routines: [RoutineDocument], workouts: [WorkoutDocument], restSeconds: Int, doneCountdownSeconds: Int, weightUnit: String, editingWorkoutID: String?, editingRoutineID: String?) -> [String: Any] {
        var dict: [String: Any] = [:]
        if let s = try? DocumentCoder.encoder.encode(routines) { dict[routinesKey] = s }
        if let w = try? DocumentCoder.encoder.encode(workouts) { dict[workoutsKey] = w }
        dict[restSecondsKey] = restSeconds
        dict[doneCountdownSecondsKey] = doneCountdownSeconds
        dict[weightUnitKey] = weightUnit
        if let editingWorkoutID { dict[editingWorkoutIDKey] = editingWorkoutID }
        if let editingRoutineID { dict[editingRoutineIDKey] = editingRoutineID }
        return dict
    }

    /// `nil` means the payload carried routines that failed to decode (a schema mismatch
    /// between the two builds) — distinct from an absent key or a legitimately empty
    /// list, so the receiver can surface it instead of silently applying nothing.
    static func decodeRoutines(_ dict: [String: Any]) -> [RoutineDocument]? {
        guard let data = dict[routinesKey] as? Data else { return [] }
        return try? DocumentCoder.decoder.decode([RoutineDocument].self, from: data)
    }

    /// See `decodeRoutines` — `nil` is a decode failure, not an empty list.
    static func decodeWorkouts(_ dict: [String: Any]) -> [WorkoutDocument]? {
        guard let data = dict[workoutsKey] as? Data else { return [] }
        return try? DocumentCoder.decoder.decode([WorkoutDocument].self, from: data)
    }

    static func decodeRestSeconds(_ dict: [String: Any]) -> Int? { dict[restSecondsKey] as? Int }

    static func decodeDoneCountdownSeconds(_ dict: [String: Any]) -> Int? { dict[doneCountdownSecondsKey] as? Int }

    static func decodeWeightUnit(_ dict: [String: Any]) -> String? { dict[weightUnitKey] as? String }

    static func decodeEditingWorkoutID(_ dict: [String: Any]) -> String? { dict[editingWorkoutIDKey] as? String }

    static func decodeEditingRoutineID(_ dict: [String: Any]) -> String? { dict[editingRoutineIDKey] as? String }

    // MARK: - Watch → Phone (a single updated workout)

    static func encodeWorkoutUpdate(_ workout: WorkoutDocument) -> [String: Any] {
        var dict: [String: Any] = [typeKey: workoutUpdateType]
        if let w = try? DocumentCoder.encoder.encode(workout) { dict[workoutKey] = w }
        return dict
    }

    static func decodeWorkoutUpdate(_ dict: [String: Any]) -> WorkoutDocument? {
        guard let data = dict[workoutKey] as? Data else { return nil }
        return try? DocumentCoder.decoder.decode(WorkoutDocument.self, from: data)
    }

    static func requestSyncMessage() -> [String: Any] { [typeKey: requestSyncType] }

    // MARK: - Watch → Phone (a setting changed on the watch)

    /// The watch's settings edits ride to the phone, which owns the durable value and
    /// echoes it back to every device through the application context. Carries only the
    /// settings the watch can edit (just rest time today); reuses the context's key, so
    /// `decodeRestSeconds` reads both.
    static func encodeSettingsUpdate(restSeconds: Int) -> [String: Any] {
        [typeKey: settingsUpdateType, restSecondsKey: restSeconds]
    }

    // MARK: - Watch → Phone (ephemeral live-run mirror)

    /// Anchor `Date`s ride as native plist values (not JSON), so they keep sub-second
    /// precision — `DocumentCoder` is `.iso8601`, which would round the timer off.
    static let lpWorkoutIDKey = "lpWorkoutID"
    static let lpLogIDKey = "lpLogID"
    static let lpNameKey = "lpName"
    static let lpPhaseKey = "lpPhase"
    static let lpSetIndexKey = "lpSetIndex"
    static let lpSetCountKey = "lpSetCount"
    static let lpDetailKey = "lpDetail"
    static let lpStartKey = "lpStart"
    static let lpEndKey = "lpEnd"
    static let lpVersionKey = "lpVersion"

    static func encodeLiveProgress(_ p: LiveProgress) -> [String: Any] {
        var dict: [String: Any] = [typeKey: liveProgressType]
        dict[lpWorkoutIDKey] = p.workoutID
        dict[lpLogIDKey] = p.logID
        dict[lpNameKey] = p.exerciseName
        dict[lpPhaseKey] = p.phase.rawValue
        dict[lpSetIndexKey] = p.setIndex
        dict[lpSetCountKey] = p.setCount
        dict[lpDetailKey] = p.detail
        dict[lpStartKey] = p.phaseStart
        if let end = p.phaseEnd { dict[lpEndKey] = end }
        dict[lpVersionKey] = p.version
        return dict
    }

    static func decodeLiveProgress(_ dict: [String: Any]) -> LiveProgress? {
        guard let workoutID = dict[lpWorkoutIDKey] as? String,
              let logID = dict[lpLogIDKey] as? String,
              let phaseRaw = dict[lpPhaseKey] as? String,
              let phase = LiveRunPhase(rawValue: phaseRaw),
              let setIndex = dict[lpSetIndexKey] as? Int,
              let setCount = dict[lpSetCountKey] as? Int,
              let start = dict[lpStartKey] as? Date,
              let version = dict[lpVersionKey] as? Int
        else { return nil }
        return LiveProgress(
            workoutID: workoutID,
            logID: logID,
            exerciseName: dict[lpNameKey] as? String ?? "",
            phase: phase,
            setIndex: setIndex,
            setCount: setCount,
            detail: dict[lpDetailKey] as? String ?? "",
            phaseStart: start,
            phaseEnd: dict[lpEndKey] as? Date,
            version: version
        )
    }

    static func encodeLiveEnded(workoutID: String, logID: String) -> [String: Any] {
        [typeKey: liveEndedType, lpWorkoutIDKey: workoutID, lpLogIDKey: logID]
    }

    // MARK: - Watch → Phone (ephemeral live heart rate)

    // Deliberately outside the `LiveProgress` machinery: HR is a device-level gauge
    // (the wearer's pulse, not a run position), updates far more often than phase
    // transitions, and must never bump the frame version sequence or re-anchor
    // timers. Best-effort, latest-wins, display-only — a dropped sample just means
    // the readout refreshes on the next one.
    static let hrBpmKey = "hrBpm"
    static let hrAtKey = "hrAt"
    static let hrKcalKey = "hrKcal"
    static let hrZoneKey = "hrZone"

    /// The session's running calories and HR zone ride along with each sample when
    /// known — absent keys (an older watch, or no energy sample yet) decode as nil,
    /// so the two builds stay wire-compatible in both directions.
    static func encodeLiveHeartRate(bpm: Double, at date: Date, kcal: Double? = nil, zone: Int? = nil) -> [String: Any] {
        var dict: [String: Any] = [typeKey: liveHeartRateType, hrBpmKey: bpm, hrAtKey: date]
        if let kcal { dict[hrKcalKey] = kcal }
        if let zone { dict[hrZoneKey] = zone }
        return dict
    }

    static func decodeLiveHeartRate(_ dict: [String: Any]) -> (bpm: Double, at: Date, kcal: Double?, zone: Int?)? {
        guard let bpm = dict[hrBpmKey] as? Double, let at = dict[hrAtKey] as? Date else { return nil }
        return (bpm, at, dict[hrKcalKey] as? Double, dict[hrZoneKey] as? Int)
    }
}
