import Foundation

/// Wraps a single `NSMetadataQuery` over the container's `Documents/` scope and
/// emits add/modify/remove events (paths relative to `Documents/`) via an
/// `AsyncStream`. `@MainActor` because `NSMetadataQuery` posts on, and must be
/// driven from, the main thread.
@MainActor
final class ICloudFileMonitor {
    enum FileChangeEvent: Sendable {
        case added(relativePath: String)
        case modified(relativePath: String)
        case removed(relativePath: String)
    }

    private let documentsURL: URL
    private var query: NSMetadataQuery?
    private var knownFiles: Set<String> = []
    private var continuation: AsyncStream<FileChangeEvent>.Continuation?

    init(documentsURL: URL) {
        self.documentsURL = documentsURL
    }

    func events() -> AsyncStream<FileChangeEvent> {
        AsyncStream { continuation in
            self.continuation = continuation
            continuation.onTermination = { @Sendable _ in
                Task { @MainActor in self.stop() }
            }
        }
    }

    func start() {
        let query = NSMetadataQuery()
        query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope]
        query.predicate = NSPredicate(format: "%K LIKE '*.json'", NSMetadataItemFSNameKey)
        self.query = query

        NotificationCenter.default.addObserver(
            self, selector: #selector(queryDidFinishGathering(_:)),
            name: .NSMetadataQueryDidFinishGathering, object: query)
        NotificationCenter.default.addObserver(
            self, selector: #selector(queryDidUpdate(_:)),
            name: .NSMetadataQueryDidUpdate, object: query)

        query.start()
    }

    func stop() {
        query?.stop()
        if let query { NotificationCenter.default.removeObserver(self, name: nil, object: query) }
        query = nil
    }

    // MARK: - Notifications

    @objc private func queryDidFinishGathering(_ notification: Notification) {
        query?.disableUpdates()
        defer { query?.enableUpdates() }
        // Seed the baseline only; do NOT emit (else every existing file would fire
        // `.added` on each launch). The engine's connect-time `reconcile()` does the
        // initial import; this query then reports only live deltas after the baseline.
        knownFiles = Set(currentRelativePaths())
    }

    @objc private func queryDidUpdate(_ notification: Notification) {
        query?.disableUpdates()
        defer { query?.enableUpdates() }

        let currentFiles = Set(currentRelativePaths())

        for file in currentFiles.subtracting(knownFiles) {
            continuation?.yield(.added(relativePath: file))
        }
        for file in knownFiles.subtracting(currentFiles) {
            continuation?.yield(.removed(relativePath: file))
        }
        if let updated = notification.userInfo?[NSMetadataQueryUpdateChangedItemsKey] as? [NSMetadataItem] {
            for item in updated {
                if let url = item.value(forAttribute: NSMetadataItemURLKey) as? URL,
                   let path = relativePath(from: url), knownFiles.contains(path) {
                    continuation?.yield(.modified(relativePath: path))
                }
            }
        }
        knownFiles = currentFiles
    }

    // MARK: - Helpers

    private func currentRelativePaths() -> [String] {
        guard let query else { return [] }
        var paths: [String] = []
        for i in 0..<query.resultCount {
            if let item = query.result(at: i) as? NSMetadataItem,
               let url = item.value(forAttribute: NSMetadataItemURLKey) as? URL,
               let path = relativePath(from: url) {
                paths.append(path)
            }
        }
        return paths
    }

    private func relativePath(from url: URL) -> String? {
        let base = documentsURL.path + "/"
        let full = url.path
        guard full.hasPrefix(base) else { return nil }
        let relative = String(full.dropFirst(base.count))
        return relative.hasSuffix(".json") ? relative : nil
    }
}
