import Foundation

/// All iCloud Drive file I/O, isolated to an actor so blocking `NSFileCoordinator`
/// calls stay off the main thread. Paths are relative to the container's
/// `Documents/` directory (e.g. `Splits/<ULID>.json`, `Stubs/<ULID>.json`).
actor ICloudFileManager {
    let documentsURL: URL

    init(containerURL: URL) {
        self.documentsURL = containerURL.appendingPathComponent("Documents", isDirectory: true)
    }

    /// Create the directory skeleton. Actor-isolated (NOT in init) so the
    /// potentially-blocking file touch runs on the actor's executor, never main.
    func prepareDirectories() {
        for sub in ["Splits", "Workouts", "Stubs"] {
            let url = documentsURL.appendingPathComponent(sub, isDirectory: true)
            try? FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
        }
    }

    // MARK: - Coordinated primitives

    func write(_ data: Data, to relativePath: String) throws {
        let fileURL = documentsURL.appendingPathComponent(relativePath)
        try FileManager.default.createDirectory(at: fileURL.deletingLastPathComponent(),
                                                withIntermediateDirectories: true)
        var coordError: NSError?
        var writeError: Error?
        NSFileCoordinator().coordinate(writingItemAt: fileURL, options: .forReplacing, error: &coordError) { url in
            do { try data.write(to: url, options: .atomic) }
            catch { writeError = error }
        }
        if let error = coordError ?? writeError { throw error }
    }

    func read(relativePath: String) throws -> Data {
        let fileURL = documentsURL.appendingPathComponent(relativePath)
        var coordError: NSError?
        var result: Result<Data, Error>?
        NSFileCoordinator().coordinate(readingItemAt: fileURL, options: [], error: &coordError) { url in
            do { result = .success(try Data(contentsOf: url)) }
            catch { result = .failure(error) }
        }
        if let coordError { throw coordError }
        switch result {
        case .success(let data): return data
        case .failure(let error): throw error
        case .none: throw CocoaError(.fileReadUnknown)
        }
    }

    func remove(relativePath: String) throws {
        let fileURL = documentsURL.appendingPathComponent(relativePath)
        guard FileManager.default.fileExists(atPath: fileURL.path) else { return }
        var coordError: NSError?
        var deleteError: Error?
        NSFileCoordinator().coordinate(writingItemAt: fileURL, options: .forDeleting, error: &coordError) { url in
            do { try FileManager.default.removeItem(at: url) }
            catch { deleteError = error }
        }
        if let error = coordError ?? deleteError { throw error }
    }

    func fileExists(_ relativePath: String) -> Bool {
        FileManager.default.fileExists(atPath: documentsURL.appendingPathComponent(relativePath).path)
    }

    // MARK: - Soft delete

    /// Writes a tombstone stub then removes the live file. Other devices learn of
    /// the delete via the stub even if they were offline for the file removal.
    func writeTombstoneAndRemove(_ tombstone: Tombstone, livePath: String) throws {
        let data = try DocumentCoder.encoder.encode(tombstone)
        try write(data, to: tombstone.relativePath)
        try remove(relativePath: livePath)
    }

    // MARK: - Enumeration

    /// Relative paths of all live data files (`Splits/…`, `Workouts/…`), excluding `Stubs/`.
    func listDataFiles() -> [String] {
        listJSON().filter { !$0.hasPrefix("Stubs/") }
    }

    /// All tombstones currently on disk.
    func listTombstones() -> [Tombstone] {
        listJSON().filter { $0.hasPrefix("Stubs/") }.compactMap { path in
            guard let data = try? read(relativePath: path) else { return nil }
            return try? DocumentCoder.decoder.decode(Tombstone.self, from: data)
        }
    }

    private func listJSON() -> [String] {
        let base = documentsURL.path + "/"
        guard let enumerator = FileManager.default.enumerator(
            at: documentsURL,
            includingPropertiesForKeys: nil,
            options: [.skipsHiddenFiles]
        ) else { return [] }
        var paths: [String] = []
        for case let url as URL in enumerator where url.pathExtension == "json" {
            let full = url.path
            if full.hasPrefix(base) { paths.append(String(full.dropFirst(base.count))) }
        }
        return paths
    }

    // MARK: - Eviction

    /// Triggers a download for an evicted file and polls until it materializes.
    func ensureDownloaded(relativePath: String) {
        let fileURL = documentsURL.appendingPathComponent(relativePath)
        guard let values = try? fileURL.resourceValues(forKeys: [.ubiquitousItemDownloadingStatusKey]),
              let status = values.ubiquitousItemDownloadingStatus, status != .current else { return }
        try? FileManager.default.startDownloadingUbiquitousItem(at: fileURL)
    }
}
