import Foundation
import os

/// Makes sure every file inside a board package actually has bytes on this device before the loader
/// is allowed to walk it.
///
/// **Why this exists at all.** `BoardLoader` is fail-fast by design: an `index.md` it cannot read is
/// a `BoardLoadError`, not a warning. On the Mac that is exactly right — an unreadable file is a real
/// defect. On the phone it is routinely a file iCloud has simply not brought down yet, or has evicted
/// to reclaim storage. Handing the loader a half-materialized package would turn ordinary sync
/// latency into the decision surface's "this board is broken", which is the wrong sentence and the
/// wrong recovery. So the sweep runs first, and a board that is not yet whole waits in a downloading
/// state instead of failing.
///
/// **A package's own metadata item is not enough to decide this.** `NSMetadataQuery` reports a
/// download status for the `.kanban` item as a whole, but that aggregate has been unreliable for
/// packages across releases and says nothing about *which* item is missing. The sweep asks each file
/// directly, which is also what lets it request the downloads.
enum PackageMaterialization {

    /// One sweep's answer.
    struct Progress: Sendable, Equatable {
        /// Items that are ubiquitous and not yet current. A download has been requested for each.
        var pending: Int

        /// Every item the walk saw, including directories and the package root.
        var total: Int

        /// The first download request that was refused, if any — best-effort observability. A refusal
        /// is not a failure of the sweep: the next sweep asks again, and the daemon usually answers
        /// the second time.
        var refusal: String?

        var isComplete: Bool { pending == 0 }

        /// 0…1 across the package, for a determinate progress view. `nil` where there is nothing to
        /// report on.
        var fractionMaterialized: Double? {
            guard total > 0 else { return nil }
            return Double(total - pending) / Double(total)
        }
    }

    private static let logger = Logger(subsystem: "dev.rzen.indie.KanbanMobile", category: "cloud")

    /// Walks the package, requests a download for every item that is not current, and answers what is
    /// still outstanding.
    ///
    /// Blocking (a full directory enumeration plus a resource-value read per item) — callers run it
    /// off the main actor.
    ///
    /// **Hidden entries are included, deliberately**: `<root>/.trash/` is materialized trash the
    /// loader reads, so a package whose trash has not come down is not yet loadable. This is the one
    /// walk in the mobile layer that does *not* use the loader's `.skipsHiddenFiles` posture.
    ///
    /// Answers `Progress(pending: 0, total: 0)` for a package under `LANEWORK_LOCAL_ROOT`, where
    /// nothing is a ubiquitous item — "complete", which is the correct reading of a folder that is
    /// simply already there.
    nonisolated static func sweep(packageAt root: URL) -> Progress {
        var progress = Progress(pending: 0, total: 0, refusal: nil)

        func consider(_ url: URL) {
            progress.total += 1
            guard !isCurrent(url) else { return }
            progress.pending += 1
            do {
                try FileManager.default.startDownloadingUbiquitousItem(at: url)
            } catch {
                if progress.refusal == nil {
                    progress.refusal = error.localizedDescription
                    logger.warning("download request refused for \(url.lastPathComponent, privacy: .public): \(error.localizedDescription, privacy: .public)")
                }
            }
        }

        consider(root)

        guard let walk = FileManager.default.enumerator(
            at: root,
            includingPropertiesForKeys: [.isUbiquitousItemKey, .ubiquitousItemDownloadingStatusKey],
            options: []
        ) else {
            return progress
        }

        for case let url as URL in walk {
            consider(url)
        }

        return progress
    }

    /// Whether one item has bytes here now.
    ///
    /// Two shapes are read as "not here". The modern one is a dataless file at its real path whose
    /// `ubiquitousItemDownloadingStatus` is `.notDownloaded`. The legacy one is a hidden `.icloud`
    /// placeholder standing where the file will land — still produced in some states, and invisible
    /// to a resource-value read on the *real* name because that name does not exist yet. Both are
    /// counted, and `startDownloadingUbiquitousItem` accepts either URL.
    ///
    /// A non-ubiquitous item (anything under the DEBUG local root, and any stray the daemon does not
    /// manage) is current by definition.
    private nonisolated static func isCurrent(_ url: URL) -> Bool {
        if url.pathExtension == "icloud", url.lastPathComponent.hasPrefix(".") {
            return false
        }
        guard let values = try? url.resourceValues(forKeys: [.isUbiquitousItemKey, .ubiquitousItemDownloadingStatusKey]),
              values.isUbiquitousItem == true
        else {
            return true
        }
        // `.downloaded` means "a local copy exists but a newer one may be in the cloud" — bytes are
        // here, which is the only question this walk asks. Only `.notDownloaded` blocks a load.
        return values.ubiquitousItemDownloadingStatus != .notDownloaded
    }
}
