//
//  DocumentSyncInspector.swift
//  Reference code — copy into your app, adjust the type/file names to taste.
//
//  Adapted from the IndieDiag package (`iCloudDocumentMonitor.swift`,
//  commit bc6f475). Two changes from the original:
//
//  1. One-shot, not always-on. The original ran a persistent
//     `NSMetadataQuery` for the app's whole lifetime and pushed updates
//     through Combine publishers. That's the icloud-sync-engine skill's
//     job (see `MetadataObserver`/`ICloudFileMonitor` there), which already
//     owns a query against the same scope to drive the local cache. This
//     type instead starts a *second*, deliberately short-lived query only
//     when a diagnostics screen wants a fresh read, and stops it once the
//     first gather completes — no ongoing duplicate query competing with
//     the sync engine's.
//
//  2. `@MainActor`, not `actor`. `NSMetadataQuery` must be started and read
//     on the main thread — the original's plain `actor` didn't actually
//     guarantee that. `@MainActor` makes it a compile-time property instead
//     of a convention (see the icloud-sync-engine skill's monitor
//     reference for the same rationale).
//
//  Combine publishers are gone in favor of a plain `async` snapshot — a
//  diagnostics screen wants "the current state," not a running feed.
//

import Foundation

/// Sync state for one document, as reported by `NSMetadataQuery`.
public enum DocumentSyncState: String, Sendable {
    case current = "Current"
    case uploading = "Uploading"
    case downloading = "Downloading"
    case notUploaded = "Not Uploaded"
    case evicted = "Evicted"
    case conflict = "Conflict"
    case error = "Error"
    case unknown = "Unknown"

    public var isActive: Bool { self == .uploading || self == .downloading }
    public var needsAttention: Bool { self == .error || self == .conflict }
}

/// A document sync/access error surfaced by `NSMetadataQuery`.
public struct DocumentSyncError: Sendable, Identifiable {
    public let id = UUID()
    public let url: URL
    public let fileName: String
    public let errorDescription: String
    public let timestamp: Date
}

/// Detailed status for one document.
public struct DocumentItemStatus: Sendable, Identifiable {
    public var id: URL { url }
    public let url: URL
    public let fileName: String
    public let syncState: DocumentSyncState
    public let uploadProgress: Double?
    public let downloadProgress: Double?
    public let error: DocumentSyncError?
    public let hasUnresolvedConflicts: Bool
    public let lastModified: Date?
    public let fileSize: Int64?
}

/// A point-in-time snapshot of every document's sync state.
public struct DocumentSyncSnapshot: Sendable {
    public let items: [DocumentItemStatus]
    public let generatedAt: Date

    public var totalCount: Int { items.count }
    public var currentCount: Int { items.filter { $0.syncState == .current }.count }
    public var evictedCount: Int { items.filter { $0.syncState == .evicted }.count }
    public var errorCount: Int { items.filter { $0.syncState == .error }.count }
    public var conflictCount: Int { items.filter { $0.hasUnresolvedConflicts }.count }
    public var isSyncing: Bool { items.contains { $0.syncState.isActive } }
    public var errors: [DocumentSyncError] { items.compactMap(\.error) }
}

/// One-shot inspector for per-document iCloud sync/eviction state. Meant to
/// be spun up when a diagnostics screen appears and torn down (deallocated)
/// when it disappears — not a second, continuously-running query living
/// alongside the sync engine's own `NSMetadataQuery`.
@MainActor
public final class DocumentSyncInspector {
    private var query: NSMetadataQuery?

    public init() {}

    /// Runs a query against the Documents/ scope of the ubiquity container,
    /// waits for the initial gather to complete (or for `timeout` to
    /// elapse, whichever comes first), captures a snapshot, and stops the
    /// query. Call this fresh each time the diagnostics screen wants an
    /// update — don't leave it running and don't call it on a timer.
    ///
    /// The timeout guards against a wedged iCloud account (the same
    /// "reverification pending" state that can hang plain file operations)
    /// leaving the query gathering forever; on timeout this returns an
    /// empty snapshot rather than hanging the caller.
    public func snapshot(
        predicate: NSPredicate = NSPredicate(value: true),
        timeout: Duration = .seconds(10)
    ) async -> DocumentSyncSnapshot {
        stop()

        let query = NSMetadataQuery()
        query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope]
        query.predicate = predicate
        self.query = query

        // Filter by identity ourselves rather than passing `query` as the
        // `object:` argument: `NSMetadataQuery` is explicitly not
        // `Sendable`, and this async sequence's `object:` parameter
        // requires it. Filtering after the fact keeps us out of another
        // query's gather-complete notification (e.g. the sync engine's own
        // long-lived `NSMetadataQuery`, if the app also uses the
        // icloud-sync-engine skill) without smuggling a non-Sendable type
        // across the sequence's internal boundary.
        let gatherComplete = NotificationCenter.default.notifications(named: .NSMetadataQueryDidFinishGathering)
        // Precompute our query's identity as a `Sendable` `ObjectIdentifier` so the
        // off-actor gather task can pick our notification out of the stream WITHOUT
        // sending the notification's non-`Sendable` `object` (`Any?`) across the
        // actor boundary — the app builds with `SWIFT_STRICT_CONCURRENCY: complete`,
        // which rejects that hop. (The reference file compared via an `await
        // self.isCurrentQuery(...)` call; that sends `Any?` to the main actor.)
        let queryID = ObjectIdentifier(query)
        query.start()

        let didGather = await withTaskGroup(of: Bool.self) { group in
            group.addTask {
                let iterator = gatherComplete.makeAsyncIterator()
                while let notification = await iterator.next() {
                    guard let object = notification.object,
                          ObjectIdentifier(object as AnyObject) == queryID else { continue }
                    return true
                }
                return false
            }
            group.addTask {
                try? await Task.sleep(for: timeout)
                return false
            }
            let first = await group.next() ?? false
            group.cancelAll()
            return first
        }

        defer { stop() }

        guard didGather, let query = self.query else {
            return DocumentSyncSnapshot(items: [], generatedAt: Date())
        }

        query.disableUpdates()
        let items = (query.results as? [NSMetadataItem] ?? []).map(Self.extractStatus)
        query.enableUpdates()

        return DocumentSyncSnapshot(items: items, generatedAt: Date())
    }

    /// Status for a single document, from a fresh snapshot. For "is this
    /// one attachment still syncing?" rather than a whole-container read.
    public func status(for url: URL) async -> DocumentItemStatus? {
        await snapshot().items.first { $0.url == url }
    }

    private func stop() {
        query?.stop()
        query = nil
    }

    private static func extractStatus(from item: NSMetadataItem) -> DocumentItemStatus {
        guard let url = item.value(forAttribute: NSMetadataItemURLKey) as? URL else {
            return DocumentItemStatus(
                url: URL(fileURLWithPath: "/unknown"),
                fileName: "Unknown",
                syncState: .unknown,
                uploadProgress: nil,
                downloadProgress: nil,
                error: nil,
                hasUnresolvedConflicts: false,
                lastModified: nil,
                fileSize: nil
            )
        }

        let fileName = item.value(forAttribute: NSMetadataItemFSNameKey) as? String ?? url.lastPathComponent

        let isUploaded = item.value(forAttribute: NSMetadataUbiquitousItemIsUploadedKey) as? Bool ?? false
        let isUploading = item.value(forAttribute: NSMetadataUbiquitousItemIsUploadingKey) as? Bool ?? false
        let isDownloading = item.value(forAttribute: NSMetadataUbiquitousItemIsDownloadingKey) as? Bool ?? false
        let uploadError = item.value(forAttribute: NSMetadataUbiquitousItemUploadingErrorKey) as? NSError
        let downloadError = item.value(forAttribute: NSMetadataUbiquitousItemDownloadingErrorKey) as? NSError
        let hasConflicts = item.value(forAttribute: NSMetadataUbiquitousItemHasUnresolvedConflictsKey) as? Bool ?? false

        // Evicted = exists in cloud, no local copy. Distinct from "not
        // uploaded yet," which is the reverse (local copy, not in cloud yet).
        let downloadStatus = item.value(forAttribute: NSMetadataUbiquitousItemDownloadingStatusKey) as? String
        let isEvicted = downloadStatus == NSMetadataUbiquitousItemDownloadingStatusNotDownloaded

        let uploadProgress = item.value(forAttribute: NSMetadataUbiquitousItemPercentUploadedKey) as? Double
        let downloadProgress = item.value(forAttribute: NSMetadataUbiquitousItemPercentDownloadedKey) as? Double
        let lastModified = item.value(forAttribute: NSMetadataItemFSContentChangeDateKey) as? Date
        let fileSize = item.value(forAttribute: NSMetadataItemFSSizeKey) as? Int64

        let syncState: DocumentSyncState
        if uploadError != nil || downloadError != nil {
            syncState = .error
        } else if hasConflicts {
            syncState = .conflict
        } else if isEvicted {
            syncState = .evicted
        } else if isUploading {
            syncState = .uploading
        } else if isDownloading {
            syncState = .downloading
        } else if isUploaded {
            syncState = .current
        } else {
            syncState = .notUploaded
        }

        let error: DocumentSyncError? = (uploadError ?? downloadError).map {
            DocumentSyncError(
                url: url,
                fileName: fileName,
                errorDescription: $0.localizedDescription,
                timestamp: Date()
            )
        }

        return DocumentItemStatus(
            url: url,
            fileName: fileName,
            syncState: syncState,
            uploadProgress: uploadProgress,
            downloadProgress: downloadProgress,
            error: error,
            hasUnresolvedConflicts: hasConflicts,
            lastModified: lastModified,
            fileSize: fileSize
        )
    }
}
