// ── Sync Engine ──────────────────────────────────────────────
// Generic offline-first sync engine for ByteLyst iOS apps.
// Provides: offline queue, periodic sync, delta pull, batch push.
// Product apps supply a SyncAdapter with their DTO types and endpoints.
//
// This extracts the generic parts of ChronoMind's PlatformSyncManager
// so every product app gets offline sync without reimplementing the
// queue, timer, error handling, and conflict resolution plumbing.

import Foundation

// MARK: - Sync Adapter Protocol

/// Product apps implement this protocol to define their sync endpoints and DTO types.
public protocol BLSyncAdapter {
    associatedtype SyncItem: Codable

    /// Pull remote changes since the given date. Return empty array if no changes.
    func pullDelta(since: Date?, client: BLPlatformClient) async throws -> [SyncItem]

    /// Push a batch of offline queue items. Return IDs of successfully synced items.
    func pushBatch(_ items: [BLOfflineQueueItem], client: BLPlatformClient) async throws -> BLBatchResult
}

// MARK: - Offline Queue Item

/// A queued change waiting to be synced.
public struct BLOfflineQueueItem: Codable {
    public let id: String
    public let action: BLSyncAction
    public let payload: Data?  // JSON-encoded product-specific DTO
    public let enqueuedAt: Date

    public init(id: String, action: BLSyncAction, payload: Data?, enqueuedAt: Date = Date()) {
        self.id = id
        self.action = action
        self.payload = payload
        self.enqueuedAt = enqueuedAt
    }
}

public enum BLSyncAction: String, Codable {
    case create
    case update
    case delete
}

// MARK: - Sync Results

public struct BLSyncResult<T> {
    public let pulled: [T]
    public let syncedIds: [String]
    public let conflicts: [BLSyncConflict]
    public let error: String?

    public init(pulled: [T], syncedIds: [String] = [], conflicts: [BLSyncConflict] = [], error: String? = nil) {
        self.pulled = pulled
        self.syncedIds = syncedIds
        self.conflicts = conflicts
        self.error = error
    }
}

public struct BLSyncConflict: Codable {
    public let id: String
    public let serverVersion: Int

    public init(id: String, serverVersion: Int) {
        self.id = id
        self.serverVersion = serverVersion
    }
}

public struct BLBatchResult: Codable {
    public let synced: [String]
    public let conflicts: [BLSyncConflict]
    public let errors: [String]

    public init(synced: [String] = [], conflicts: [BLSyncConflict] = [], errors: [String] = []) {
        self.synced = synced
        self.conflicts = conflicts
        self.errors = errors
    }
}

// MARK: - Sync Engine

/// Generic sync engine. Product apps create one instance with their SyncAdapter.
public final class BLSyncEngine<Adapter: BLSyncAdapter> {

    private let config: BLPlatformConfig
    private let client: BLPlatformClient
    private let adapter: Adapter

    private let storagePrefix: String
    private var syncTask: Task<Void, Never>?
    private let syncIntervalSec: TimeInterval

    // MARK: - State

    public private(set) var isSyncing = false
    public private(set) var lastSyncDate: Date?
    public private(set) var pendingChanges: Int = 0
    public private(set) var lastError: String?

    public var syncEnabled: Bool {
        didSet {
            UserDefaults.standard.set(syncEnabled, forKey: "\(storagePrefix)-sync-enabled")
            if syncEnabled { schedulePeriodicSync() } else { cancelPeriodicSync() }
        }
    }

    /// Called when sync state changes (for UI binding).
    public var onStateChanged: (() -> Void)?

    public init(
        config: BLPlatformConfig,
        client: BLPlatformClient,
        adapter: Adapter,
        syncIntervalSec: TimeInterval = 60
    ) {
        self.config = config
        self.client = client
        self.adapter = adapter
        self.syncIntervalSec = syncIntervalSec
        self.storagePrefix = config.productId

        syncEnabled = UserDefaults.standard.bool(forKey: "\(storagePrefix)-sync-enabled")

        if let date = UserDefaults.standard.object(forKey: "\(storagePrefix)-last-sync") as? Date {
            lastSyncDate = date
        }

        pendingChanges = loadQueueItems().count

        if syncEnabled {
            schedulePeriodicSync()
        }
    }

    // MARK: - Sync

    /// Full delta sync: pull remote changes, push offline queue.
    public func sync() async -> BLSyncResult<Adapter.SyncItem> {
        guard syncEnabled, client.authToken != nil else {
            return BLSyncResult(pulled: [], error: "Not authenticated or sync disabled")
        }

        isSyncing = true
        lastError = nil
        onStateChanged?()
        defer {
            isSyncing = false
            onStateChanged?()
        }

        do {
            let pulled = try await adapter.pullDelta(since: lastSyncDate, client: client)
            let batchResult = try await pushOfflineQueue()

            lastSyncDate = Date()
            UserDefaults.standard.set(lastSyncDate, forKey: "\(storagePrefix)-last-sync")

            return BLSyncResult(
                pulled: pulled,
                syncedIds: batchResult.synced,
                conflicts: batchResult.conflicts,
                error: nil
            )
        } catch {
            lastError = error.localizedDescription
            return BLSyncResult(pulled: [], error: error.localizedDescription)
        }
    }

    // MARK: - Offline Queue

    /// Enqueue a change for later sync.
    public func enqueueChange(id: String, action: BLSyncAction, payload: Data?) {
        var queue = loadQueueItems()
        queue.removeAll { $0.id == id }
        queue.append(BLOfflineQueueItem(id: id, action: action, payload: payload))
        saveQueue(queue)
        pendingChanges = queue.count
        onStateChanged?()
    }

    /// Enqueue a delete.
    public func enqueueDelete(id: String) {
        enqueueChange(id: id, action: .delete, payload: nil)
    }

    // MARK: - Private

    private func pushOfflineQueue() async throws -> BLBatchResult {
        let queue = loadQueueItems()
        guard !queue.isEmpty else {
            return BLBatchResult()
        }

        let result = try await adapter.pushBatch(queue, client: client)

        // Clear synced items
        var remaining = loadQueueItems()
        remaining.removeAll { result.synced.contains($0.id) }
        saveQueue(remaining)
        pendingChanges = remaining.count

        return result
    }

    private func schedulePeriodicSync() {
        cancelPeriodicSync()
        syncTask = Task { [weak self] in
            while !Task.isCancelled {
                try? await Task.sleep(nanoseconds: UInt64((self?.syncIntervalSec ?? 60) * 1_000_000_000))
                guard let self, self.syncEnabled else { break }
                _ = await self.sync()
            }
        }
    }

    private func cancelPeriodicSync() {
        syncTask?.cancel()
        syncTask = nil
    }

    // MARK: - Queue Persistence

    private func loadQueueItems() -> [BLOfflineQueueItem] {
        guard let data = UserDefaults.standard.data(forKey: "\(storagePrefix)-offline-queue") else { return [] }
        return (try? JSONDecoder().decode([BLOfflineQueueItem].self, from: data)) ?? []
    }

    private func saveQueue(_ queue: [BLOfflineQueueItem]) {
        if let data = try? JSONEncoder().encode(queue) {
            UserDefaults.standard.set(data, forKey: "\(storagePrefix)-offline-queue")
        }
    }
}
