import Foundation
import Observation
import SwiftData
import WatchConnectivity

/// Watch side of the iPhone↔Watch bridge. The watch never touches iCloud — it
/// keeps a local SwiftData cache fed only by application-context pushes from the
/// phone, updates it optimistically on local edits, and forwards changed workouts
/// to the phone (which is the sole writer of iCloud Drive).
@Observable
@MainActor
final class WatchConnectivityBridge: NSObject {
    private let container: ModelContainer
    private var session: WCSession?

    /// Last time state was received from the phone (for a sync indicator).
    private(set) var lastSyncDate: Date?

    private var context: ModelContext { container.mainContext }

    init(container: ModelContainer) {
        self.container = container
        super.init()
    }

    func activate() {
        guard WCSession.isSupported() else { return }
        let session = WCSession.default
        session.delegate = self
        session.activate()
        self.session = session
        // Apply whatever the phone last pushed, then ask for a fresh push.
        applyState(WCPayload.decodeSplits(session.receivedApplicationContext),
                   workouts: WCPayload.decodeWorkouts(session.receivedApplicationContext))
        applyRestSeconds(session.receivedApplicationContext)
        requestSync()
    }

    func requestSync() {
        guard let session, session.activationState == .activated, session.isReachable else { return }
        session.sendMessage(WCPayload.requestSyncMessage(), replyHandler: nil, errorHandler: nil)
    }

    /// Optimistically applies a workout edit to the local cache and forwards it to
    /// the phone for durable persistence in iCloud Drive.
    func update(workout doc: WorkoutDocument) {
        CacheMapper.upsertWorkout(doc, relativePath: doc.relativePath, into: context)
        try? context.save()
        sendToPhone(doc)
    }

    // MARK: - Internal

    private func sendToPhone(_ doc: WorkoutDocument) {
        guard let session, session.activationState == .activated else { return }
        let payload = WCPayload.encodeWorkoutUpdate(doc)
        if session.isReachable {
            session.sendMessage(payload, replyHandler: nil, errorHandler: { _ in
                session.transferUserInfo(payload)   // fall back to guaranteed delivery
            })
        } else {
            session.transferUserInfo(payload)
        }
    }

    private func applyRestSeconds(_ dict: [String: Any]) {
        if let rest = WCPayload.decodeRestSeconds(dict) {
            UserDefaults.standard.set(rest, forKey: WCPayload.restSecondsKey)
        }
    }

    private func applyState(_ splits: [SplitDocument], workouts: [WorkoutDocument]) {
        guard !splits.isEmpty || !workouts.isEmpty else { return }
        var liveSplitIDs = Set<String>()
        for s in splits {
            CacheMapper.upsertSplit(s, relativePath: s.relativePath, into: context)
            liveSplitIDs.insert(s.id)
        }
        for w in workouts {
            CacheMapper.upsertWorkout(w, relativePath: w.relativePath, into: context)
        }
        // Splits are sent in full → prune any the phone no longer has. Workouts are
        // sent as a recent window, so they're upserted but never pruned (avoids a
        // race deleting a workout just created on the watch).
        if let allSplits = try? context.fetch(FetchDescriptor<Split>()) {
            for s in allSplits where !liveSplitIDs.contains(s.id) { context.delete(s) }
        }
        try? context.save()
        lastSyncDate = Date()
    }
}

// MARK: - WCSessionDelegate

extension WatchConnectivityBridge: WCSessionDelegate {
    nonisolated func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
        Task { @MainActor in self.requestSync() }
    }

    nonisolated func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String: Any]) {
        let splits = WCPayload.decodeSplits(applicationContext)
        let workouts = WCPayload.decodeWorkouts(applicationContext)
        let rest = WCPayload.decodeRestSeconds(applicationContext)
        Task { @MainActor in
            self.applyState(splits, workouts: workouts)
            if let rest { UserDefaults.standard.set(rest, forKey: WCPayload.restSecondsKey) }
        }
    }
}
