import Foundation

/// `NSFileCoordinator` brackets around the storage layer's own I/O.
///
/// **Required, not defensive.** `BoardLoader` and `BoardWriter` read and write with plain
/// `FileManager` calls, which is correct on the Mac where the app owns the folder outright. In a
/// ubiquity container the daemon is a second writer: it materializes, evicts and replaces items
/// underneath a walk with no warning. Coordination is the only thing that makes "the tree did not
/// move while I read it" true, and the only thing that tells the daemon not to push a remote version
/// into a folder mid-write.
///
/// The bracket wraps the **package root**, not each file inside it. A board is one document
/// (`LSTypeIsPackage`), so one coordination covers the whole walk — which is also the only shape
/// that can hold a multi-file write (a move is two folders, a delete is a folder plus its trash
/// destination) as one unit.
///
/// **Every call blocks.** `coordinate` waits for the daemon and for other presenters, so these run
/// off the main actor without exception; the types here are `nonisolated` and stateless so they can.
enum CoordinatedFileAccess {

    /// Runs `body` under a read intent on `url`, and answers what it returned.
    ///
    /// `body` is handed the URL the coordinator resolved — which may differ from `url` if the item
    /// moved — and must use it rather than closing over the original. It is deliberately
    /// non-throwing: the storage layer's typed errors (`BoardLoadFailure`, `BoardWriteError`) are far
    /// richer than anything this layer could wrap, so a caller returns its own `Result` from `body`
    /// and the outer `Result` carries only the coordinator's own refusal.
    static func read<T>(
        itemAt url: URL,
        options: NSFileCoordinator.ReadingOptions = [],
        by body: (URL) -> T
    ) -> Result<T, CoordinationFailure> {
        var captured: T?
        var ran = false
        var coordinatorError: NSError?
        let coordinator = NSFileCoordinator(filePresenter: nil)
        coordinator.coordinate(readingItemAt: url, options: options, error: &coordinatorError) { resolved in
            ran = true
            captured = body(resolved)
        }
        // `ran` rather than `captured != nil`: a `T` that is itself optional would otherwise read a
        // legitimate nil result as "the block never ran".
        guard ran, let captured else {
            return .failure(CoordinationFailure(coordinatorError, fallback: "the coordinated read did not run"))
        }
        return .success(captured)
    }

    /// Runs `body` under a write intent on `url` — the bracket every `BoardWriter` call on the phone
    /// goes through. Same contract as `read(itemAt:options:by:)`.
    static func write<T>(
        itemAt url: URL,
        options: NSFileCoordinator.WritingOptions = [],
        by body: (URL) -> T
    ) -> Result<T, CoordinationFailure> {
        var captured: T?
        var ran = false
        var coordinatorError: NSError?
        let coordinator = NSFileCoordinator(filePresenter: nil)
        coordinator.coordinate(writingItemAt: url, options: options, error: &coordinatorError) { resolved in
            ran = true
            captured = body(resolved)
        }
        guard ran, let captured else {
            return .failure(CoordinationFailure(coordinatorError, fallback: "the coordinated write did not run"))
        }
        return .success(captured)
    }
}

/// The coordinator refused — a lock it could not take, an item it could not reach.
///
/// A flattened value rather than the `NSError` itself: this crosses from a detached task back to the
/// main actor, and three `Sendable` scalars carry everything a log line or an alert needs without
/// smuggling a reference type across the boundary.
struct CoordinationFailure: Error, Sendable, Equatable, CustomStringConvertible {
    let domain: String
    let code: Int
    let message: String

    init(_ error: NSError?, fallback: String) {
        domain = error?.domain ?? "dev.rzen.indie.KanbanMobile.coordination"
        code = error?.code ?? -1
        message = error?.localizedDescription ?? fallback
    }

    var description: String { "\(message) (\(domain) \(code))" }
}
