//
//  CloudSyncManager.swift
//  CasinoKit
//
//  Generic iCloud sync manager for casino game data.
//

import Foundation
import SwiftUI

/// Protocol for game data that can be persisted and synced.
public protocol PersistableGameData: Codable, Sendable {
    /// Unique identifier for this game's data (e.g., "baccarat", "blackjack").
    static var gameIdentifier: String { get }
    
    /// The number of rounds/hands played - used to determine which data is newer.
    var roundsPlayed: Int { get }
    
    /// Last time this data was modified.
    var lastModified: Date { get set }
    
    /// Creates empty/default game data.
    static var empty: Self { get }
}

/// Manages game data persistence to local storage and iCloud.
@MainActor
@Observable
public final class CloudSyncManager<T: PersistableGameData> {
    
    // MARK: - Properties
    
    /// The current game data.
    public private(set) var data: T
    
    /// Whether iCloud sync is available (user signed in).
    public var iCloudAvailable: Bool {
        let token = FileManager.default.ubiquityIdentityToken
        let available = token != nil
        CasinoDesign.debugLog("CloudSyncManager: iCloud available = \(available), token = \(String(describing: token))")
        return available
    }
    
    /// Whether iCloud sync is enabled by the user.
    public var iCloudEnabled: Bool {
        get { UserDefaults.standard.bool(forKey: iCloudEnabledKey) }
        set { 
            UserDefaults.standard.set(newValue, forKey: iCloudEnabledKey)
            if newValue { sync() }
        }
    }
    
    /// Last successful sync date.
    public private(set) var lastSyncDate: Date?
    
    /// Whether a sync is in progress.
    public private(set) var isSyncing: Bool = false
    
    /// Sync status message.
    public private(set) var syncStatus: String = ""
    
    // MARK: - Private Properties
    
    private let iCloudStore = NSUbiquitousKeyValueStore.default
    private let encoder = JSONEncoder()
    private let decoder = JSONDecoder()
    
    private var localKey: String { "\(T.gameIdentifier).gameData" }
    private var cloudKey: String { "\(T.gameIdentifier).gameData" }
    private var syncDateKey: String { "\(T.gameIdentifier).lastSync" }
    private var iCloudEnabledKey: String { "\(T.gameIdentifier).iCloudEnabled" }
    
    /// Callback when data changes from iCloud.
    public var onCloudDataReceived: ((T) -> Void)?
    
    /// Whether initial iCloud sync has completed.
    public private(set) var hasCompletedInitialSync: Bool = false
    
    // MARK: - Initialization
    
    public init() {
        // Start with empty data
        self.data = T.empty
        
        // Set default iCloud enabled to true
        if UserDefaults.standard.object(forKey: iCloudEnabledKey) == nil {
            UserDefaults.standard.set(true, forKey: iCloudEnabledKey)
        }
        
        // Register for iCloud changes BEFORE syncing
        NotificationCenter.default.addObserver(
            forName: NSUbiquitousKeyValueStore.didChangeExternallyNotification,
            object: iCloudStore,
            queue: .main
        ) { [weak self] notification in
            // Extract values before crossing isolation boundary (for Sendable compliance)
            guard let userInfo = notification.userInfo,
                  let reason = userInfo[NSUbiquitousKeyValueStoreChangeReasonKey] as? Int,
                  let changedKeys = userInfo[NSUbiquitousKeyValueStoreChangedKeysKey] as? [String] else {
                return
            }
            Task { @MainActor in
                self?.handleCloudChange(reason: reason, changedKeys: changedKeys)
            }
        }
        
        // Trigger iCloud sync FIRST (before loading local)
        if iCloudAvailable && iCloudEnabled {
            iCloudStore.synchronize()
        }
        
        // Load data (may get updated when iCloud sync completes)
        self.data = load()
        
        // On fresh install, wait briefly for iCloud data to arrive
        if data.roundsPlayed == 0 && iCloudAvailable && iCloudEnabled {
            scheduleDelayedCloudCheck()
        }
    }
    
    /// Checks for iCloud data after a brief delay (for fresh installs).
    private func scheduleDelayedCloudCheck() {
        CasinoDesign.debugLog("CloudSyncManager[\(T.gameIdentifier)]: Scheduling delayed cloud check...")
        
        Task { @MainActor in
            // Wait for iCloud to sync (typically takes 1-2 seconds on fresh install)
            try? await Task.sleep(for: .seconds(2))
            
            CasinoDesign.debugLog("CloudSyncManager[\(T.gameIdentifier)]: Delayed check - forcing sync...")
            
            // Force another sync (on main thread to avoid concurrency warning)
            var syncResult = false
            await MainActor.run {
                syncResult = iCloudStore.synchronize()
            }
            CasinoDesign.debugLog("CloudSyncManager[\(T.gameIdentifier)]: synchronize() returned \(syncResult)")
            
            // Check what's in the store
            let allKeys = iCloudStore.dictionaryRepresentation.keys
            CasinoDesign.debugLog("CloudSyncManager[\(T.gameIdentifier)]: iCloud store keys: \(Array(allKeys))")
            
            // Try loading cloud data again
            if let cloudData = loadCloud() {
                CasinoDesign.debugLog("CloudSyncManager[\(T.gameIdentifier)]: Found cloud data with \(cloudData.roundsPlayed) rounds")
                if cloudData.roundsPlayed > data.roundsPlayed {
                    CasinoDesign.debugLog("CloudSyncManager[\(T.gameIdentifier)]: Cloud has more data, updating...")
                    data = cloudData
                    hasCompletedInitialSync = true
                    onCloudDataReceived?(cloudData)
                    
                    // Save to local as well
                    if let encoded = try? encoder.encode(cloudData) {
                        UserDefaults.standard.set(encoded, forKey: localKey)
                    }
                    
                    // Post notification
                    NotificationCenter.default.post(
                        name: .casinoGameDataDidChange,
                        object: nil,
                        userInfo: ["gameIdentifier": T.gameIdentifier]
                    )
                } else {
                    CasinoDesign.debugLog("CloudSyncManager[\(T.gameIdentifier)]: Local data has same or more rounds")
                }
            } else {
                hasCompletedInitialSync = true
                CasinoDesign.debugLog("CloudSyncManager[\(T.gameIdentifier)]: No cloud data found after delay")
            }
        }
    }
    
    // MARK: - Save
    
    /// Saves the current game data locally and to iCloud.
    public func save(_ newData: T) {
        var dataToSave = newData
        dataToSave.lastModified = Date()
        self.data = dataToSave
        
        guard let encoded = try? encoder.encode(dataToSave) else {
            CasinoDesign.debugLog("CloudSyncManager: Failed to encode game data")
            return
        }
        
        // Save locally
        UserDefaults.standard.set(encoded, forKey: localKey)
        
        // Save to iCloud
        if iCloudAvailable && iCloudEnabled {
            iCloudStore.set(encoded, forKey: cloudKey)
            iCloudStore.set(Date(), forKey: syncDateKey)
            iCloudStore.synchronize()
            lastSyncDate = Date()
            syncStatus = "Synced"
        }
        
        CasinoDesign.debugLog("CloudSyncManager[\(T.gameIdentifier)]: Saved (rounds: \(dataToSave.roundsPlayed))")
    }
    
    /// Convenience to update and save in one call.
    public func update(_ transform: (inout T) -> Void) {
        var updated = data
        transform(&updated)
        save(updated)
    }
    
    // MARK: - Load
    
    /// Loads game data, preferring iCloud if it has more progress.
    public func load() -> T {
        let localData = loadLocal()
        let cloudData = loadCloud()
        
        // Determine which data to use
        let finalData: T
        
        switch (localData, cloudData) {
        case (nil, nil):
            CasinoDesign.debugLog("CloudSyncManager[\(T.gameIdentifier)]: No saved data, using empty")
            finalData = T.empty
            
        case (let local?, nil):
            CasinoDesign.debugLog("CloudSyncManager[\(T.gameIdentifier)]: Using local data")
            finalData = local
            
        case (nil, let cloud?):
            CasinoDesign.debugLog("CloudSyncManager[\(T.gameIdentifier)]: Using iCloud data")
            finalData = cloud
            
        case (let local?, let cloud?):
            // Use whichever has more rounds played
            if cloud.roundsPlayed > local.roundsPlayed {
                CasinoDesign.debugLog("CloudSyncManager[\(T.gameIdentifier)]: Using iCloud (more progress: \(cloud.roundsPlayed) vs \(local.roundsPlayed))")
                finalData = cloud
                // Update local with cloud data
                if let encoded = try? encoder.encode(cloud) {
                    UserDefaults.standard.set(encoded, forKey: localKey)
                }
            } else if local.lastModified > cloud.lastModified {
                CasinoDesign.debugLog("CloudSyncManager[\(T.gameIdentifier)]: Using local (newer: \(local.lastModified))")
                finalData = local
            } else {
                CasinoDesign.debugLog("CloudSyncManager[\(T.gameIdentifier)]: Using local data")
                finalData = local
            }
        }
        
        return finalData
    }
    
    private func loadLocal() -> T? {
        guard let data = UserDefaults.standard.data(forKey: localKey),
              let decoded = try? decoder.decode(T.self, from: data) else {
            return nil
        }
        return decoded
    }
    
    private func loadCloud() -> T? {
        guard iCloudAvailable && iCloudEnabled,
              let data = iCloudStore.data(forKey: cloudKey),
              let decoded = try? decoder.decode(T.self, from: data) else {
            return nil
        }
        
        if let syncDate = iCloudStore.object(forKey: syncDateKey) as? Date {
            lastSyncDate = syncDate
        }
        
        return decoded
    }
    
    // MARK: - Sync
    
    /// Forces a sync with iCloud.
    public func sync() {
        guard iCloudAvailable && iCloudEnabled else {
            syncStatus = iCloudAvailable ? "Sync disabled" : "iCloud unavailable"
            return
        }
        
        isSyncing = true
        syncStatus = "Syncing..."
        
        iCloudStore.synchronize()
        
        // Reload to get any changes
        let latestData = load()
        if latestData.roundsPlayed != data.roundsPlayed {
            data = latestData
            onCloudDataReceived?(latestData)
        }
        
        isSyncing = false
        lastSyncDate = Date()
        syncStatus = "Synced"
    }
    
    // MARK: - Cloud Change Handling
    
    private func handleCloudChange(reason: Int, changedKeys: [String]) {
        // Check if our data changed
        guard changedKeys.contains(cloudKey) else { return }
        
        switch reason {
        case NSUbiquitousKeyValueStoreServerChange,
             NSUbiquitousKeyValueStoreInitialSyncChange:
            CasinoDesign.debugLog("CloudSyncManager[\(T.gameIdentifier)]: Data changed from another device")
            syncStatus = "Received update"
            
            // Reload and notify
            if let cloudData = loadCloud(), cloudData.roundsPlayed > data.roundsPlayed {
                data = cloudData
                onCloudDataReceived?(cloudData)
                
                // Also update local
                if let encoded = try? encoder.encode(cloudData) {
                    UserDefaults.standard.set(encoded, forKey: localKey)
                }
                
                // Post notification
                NotificationCenter.default.post(
                    name: .casinoGameDataDidChange,
                    object: nil,
                    userInfo: ["gameIdentifier": T.gameIdentifier]
                )
            }
            
        case NSUbiquitousKeyValueStoreQuotaViolationChange:
            CasinoDesign.debugLog("CloudSyncManager[\(T.gameIdentifier)]: iCloud quota exceeded")
            syncStatus = "Storage full"
            
        case NSUbiquitousKeyValueStoreAccountChange:
            CasinoDesign.debugLog("CloudSyncManager[\(T.gameIdentifier)]: iCloud account changed")
            syncStatus = "Account changed"
            // Reload with new account
            data = load()
            
        default:
            break
        }
    }
    
    // MARK: - Reset
    
    /// Clears all saved data locally and from iCloud.
    public func reset() {
        UserDefaults.standard.removeObject(forKey: localKey)
        
        if iCloudAvailable {
            iCloudStore.removeObject(forKey: cloudKey)
            iCloudStore.removeObject(forKey: syncDateKey)
            iCloudStore.synchronize()
        }
        
        data = T.empty
        syncStatus = "Data cleared"
        CasinoDesign.debugLog("CloudSyncManager[\(T.gameIdentifier)]: All data cleared")
    }
}

// MARK: - Notifications

public extension Notification.Name {
    /// Posted when game data changes from iCloud.
    static let casinoGameDataDidChange = Notification.Name("casinoGameDataDidChange")
}

