// SPDX-License-Identifier: MIT
// Copyright (c) 2026 Thibault Ducray
//
// This file is part of MyPwdTool's open-source sync/encryption core — see
// LICENSE-SYNC-CRYPTO.md at the repo root and https://mypwdtool.com/open-source/.
// The rest of this application is proprietary and NOT covered by this license.

import Foundation

// MARK: - SyncOutbox

/// Simple in-memory outbox for pending sync changes.
/// Thread-safe via NSLock.
final class SyncOutbox {

    static let shared = SyncOutbox()

    struct Item: Identifiable {
        let id = UUID()
        let op: SyncOp
        let entryId: UUID
        let updatedAt: Date
        let entry: Entry?       // nil for delete
    }

    private var items: [Item] = []
    private let lock = NSLock()

    private init() {}

    /// Enqueue an item for sending.
    func enqueue(_ item: Item) {
        lock.lock()
        defer { lock.unlock() }
        items.append(item)
    }

    /// Returns and clears all pending items atomically.
    func drain() -> [Item] {
        lock.lock()
        defer { lock.unlock() }
        let result = items
        items = []
        return result
    }

    /// True if there are no pending items.
    var isEmpty: Bool {
        lock.lock()
        defer { lock.unlock() }
        return items.isEmpty
    }
}

// MARK: - SyncControlOutbox

/// Retry queue for one-shot group-control messages (device_revoke, device_leave, key_rotate,
/// device_hello, peer introductions) — everything sync sends besides an entry upsert/delete and
/// a heartbeat. These used to be pure fire-and-forget: if the send failed (most commonly, the
/// device being offline at that exact moment), the action was still applied locally but the rest
/// of the group never learned about it until the user manually retried the same action. Found
/// live 2026-09-18: an offline revoke, and a self-leave, both "worked" locally but never reached
/// the other devices even once the network came back.
///
/// Unlike `SyncOutbox` (which carries an already-fully-formed snapshot to resend as-is), an
/// `Item` here is a closure that *re-derives* its own payload from current `SyncStore` state each
/// time it runs — recipients and the current SGK may well have changed between the first attempt
/// and a retry (e.g. a second revoke happened while still offline), and re-deriving fresh is
/// always safe here, whereas replaying a frozen snapshot could target a stale recipient list or
/// an already-superseded key. The one exception is `device_leave`, whose closure captures a
/// frozen snapshot instead — `disableSync()` wipes the local SyncStore right after attempting the
/// send, so by retry time there is no "current state" left to re-derive from (see its own call
/// site for why).
///
/// Drained alongside `SyncOutbox` by `sendOutboxItems()`, so it rides the exact same retry
/// cadence — the poll loop's safety net, `kickDrain()`, and engine start — with no separate
/// scheduling needed. In-memory only, like `SyncOutbox`: doesn't survive a full app relaunch
/// while still offline, matching that same pre-existing, accepted limitation.
final class SyncControlOutbox {

    static let shared = SyncControlOutbox()

    struct Item {
        let id = UUID()
        let label: String              // for the retry-failure log line only
        let send: () async -> Bool     // re-derives its own payload; true = delivered (or moot)
    }

    private var items: [Item] = []
    private let lock = NSLock()

    private init() {}

    func enqueue(_ item: Item) {
        lock.lock()
        defer { lock.unlock() }
        items.append(item)
    }

    func drain() -> [Item] {
        lock.lock()
        defer { lock.unlock() }
        let result = items
        items = []
        return result
    }

    var isEmpty: Bool {
        lock.lock()
        defer { lock.unlock() }
        return items.isEmpty
    }
}
