import Foundation
import AppKit

typealias StorageErrorCallback = (StorageError) -> Void

struct SecurityBookmark {

    enum OverwriteBehaviour {
        case skip
        case write
        case writeIfChanged
        case fail
    }

    let url: URL

    let isStale: Bool

    private let encoder = JSONEncoder()

    private let decoder = JSONDecoder()

    private let fm = FileManager.default

    var errorNotification: StorageErrorCallback?

    init(url: URL, isStale: Bool) {
        self.url = url
        self.isStale = isStale

        self.encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
    }

    private func reportError(_ error: String) {
        guard let errorNotification else {
            print(error)
            return
        }
        errorNotification(.init(message: error))
    }

    // MARK: Write

    func openFinderWindow(relativePath: String) {
        with(relativePath: relativePath) { path in
            NSWorkspace.shared.activateFileViewerSelecting([path])
        }
    }

    func fullPath(to relativePath: String) -> URL {
        return url.appending(path: relativePath.withLeadingSlashRemoved, directoryHint: .notDirectory)
    }

    /**
     Write the data of an encodable value to a relative path in the content folder,
     or delete the file if nil is passed.
     */
    func encode<T>(_ value: T?, to relativePath: String) -> Bool where T: Encodable {
        guard let value else {
            return deleteFile(at: relativePath)
        }
        return encode(value, to: relativePath)
    }

    /**
     Write the data of an encodable value to a relative path in the content folder
     */
    func encode<T>(_ value: T, to relativePath: String) -> Bool where T: Encodable {
        let data: Data
        do {
            data = try encoder.encode(value)
        } catch {
            reportError("Failed to encode \(value): \(error)")
            return false
        }
        return write(data, to: relativePath)
    }

    /**
     Write text to a file at the relative path.
     */
    func write(_ content: String,
               to relativePath: String,
               createParentFolder: Bool = true,
               ifFileExists overwrite: OverwriteBehaviour = .writeIfChanged) -> Bool {
        guard let data = content.data(using: .utf8) else {
            reportError("Failed to encode content to write to \(relativePath)")
            return false
        }
        return write(data, to: relativePath, createParentFolder: createParentFolder, ifFileExists: overwrite)
    }

    func write(_ data: Data,
               to relativePath: String,
               createParentFolder: Bool = true,
               ifFileExists overwrite: OverwriteBehaviour = .writeIfChanged) -> Bool {
        perform { url in
            let file = fullPath(to: relativePath)

            if exists(file) {
                switch overwrite {
                case .fail:
                    reportError("Failed to write \(relativePath): File exists")
                    return false
                case .skip: return true
                case .write: break
                case .writeIfChanged:
                    if let existingData = try? Data(contentsOf: file),
                       existingData == data {
                        return true
                    }
                }
            }
            do {
                try createParentIfNeeded(of: file)
                try data.write(to: file)
            } catch {
                reportError("Failed to write \(relativePath): \(error)")
                return false
            }
            return true
        }
    }

    func create(folder: String) -> Bool {
        with(relativePath: folder, perform: create)
    }

    // MARK: Read

    func size(of relativePath: String) -> Int? {
        with(relativePath: relativePath) { $0.size }
    }

    func hasFile(at relativePath: String) -> Bool {
        with(relativePath: relativePath, perform: exists)
    }

    func readString(at relativePath: String) -> String? {
        guard let data = readData(at: relativePath) else {
            return nil
        }
        guard let result = String(data: data, encoding: .utf8) else {
            reportError("Failed to read \(relativePath): invalid UTF-8")
            return nil
        }
        return result
    }

    func readData(at relativePath: String) -> Data? {
        with(relativePath: relativePath) { file in
            guard exists(file) else {
                return nil
            }
            do {
                return try Data(contentsOf: file)
            } catch {
                reportError("Failed to read \(relativePath) \(error)")
                return nil
            }
        }
    }

    func decode<T>(at relativePath: String) -> T? where T: Decodable {
        guard let data = readData(at: relativePath) else {
            return nil
        }
        do {
            return try decoder.decode(T.self, from: data)
        } catch {
            reportError("Failed to decode \(relativePath): \(error)")
            return nil
        }
    }

    // MARK: Modify

    func move(_ relativeSource: String,
              to relativeDestination: String,
              failIfMissing: Bool = true,
              createParentFolder: Bool = true,
              ifFileExists overwrite: OverwriteBehaviour = .fail) -> Bool {
        with(relativePath: relativeSource) { source in
            if !exists(source) {
                if !failIfMissing { return true }
                reportError("Failed to move \(relativeSource): File \(source) does not exist")
                return false
            }

            let destination = url.appending(path: relativeDestination.withLeadingSlashRemoved)
            if exists(destination) {
                switch overwrite {
                case .fail:
                    reportError("Failed to move to \(relativeDestination): File already exists")
                    return false
                case .skip: return true
                case .write: break
                case .writeIfChanged:
                    if let existingData = try? Data(contentsOf: destination),
                       let newData = try? Data(contentsOf: source),
                       existingData == newData {
                        return true
                    }
                }
            }
            do {
                if createParentFolder {
                    try createParentIfNeeded(of: destination)
                }
                try fm.moveItem(at: source, to: destination)
                return true
            } catch {
                reportError("Failed to move \(source.path()) to \(destination.path()): \(error)")
                return false
            }
        }
    }

    func copy(externalFile: URL,
              to relativePath: String,
              createParentFolder: Bool = true,
              ifFileExists overwrite: OverwriteBehaviour = .writeIfChanged) -> Bool {
        with(relativePath: relativePath) { destination in
            do {
                if destination.exists {
                    switch overwrite {
                    case .fail:
                        reportError("Failed to copy to \(relativePath): File already exists")
                        return false
                    case .skip: return true
                    case .write: break
                    case .writeIfChanged:
                        if let existingData = try? Data(contentsOf: destination),
                           let newData = try? Data(contentsOf: externalFile),
                           existingData == newData {
                            return true
                        }
                    }
                    try fm.removeItem(at: destination)
                }
                try createParentIfNeeded(of: destination)
                try fm.copyItem(at: externalFile, to: destination)
                return true
            } catch {
                reportError("Failed to copy \(externalFile.path()) to \(relativePath): \(error)")
                return false
            }
        }
    }

    func deleteFile(at relativePath: String) -> Bool {
        with(relativePath: relativePath) { file in
            guard exists(file) else {
                return true
            }
            do {
                try fm.removeItem(at: file)
                return true
            } catch {
                reportError("Failed to delete \(relativePath): \(error)")
                return false
            }
        }
    }

    // MARK: Writing files

    /**
     Delete files in a subPath of the content folder which are not in the given set of files
     - Note: This function requires a security scope for the content path
     */
    func deleteFiles(in relativePath: String, notIn fileSet: Set<String>) -> [String]? {
        with(relativePath: relativePath) { folder in
            if !exists(folder) {
                return []
            }
            guard let files = files(in: folder) else {
                return []
            }
            return files.compactMap { file in
                guard !fileSet.contains(file.lastPathComponent) else {
                    return nil
                }
                guard remove(file) else {
                    return nil
                }
                return file.lastPathComponent
            }
        }
    }

    // MARK: Transfer

    func transfer(file sourcePath: String,
                  to relativePath: String,
                  of scope: SecurityBookmark,
                  createParentFolder: Bool = true,
                  ifFileExists: OverwriteBehaviour = .writeIfChanged) -> Bool {
        with(relativePath: sourcePath) { source in
            scope.copy(
                externalFile: source,
                to: relativePath,
                createParentFolder: createParentFolder,
                ifFileExists: ifFileExists)
        }
    }

    // MARK: Batch operations

    func fileNames(inRelativeFolder relativePath: String) -> [String]? {
        files(inRelativeFolder: relativePath)?.map { $0.lastPathComponent }
    }

    func files(inRelativeFolder relativePath: String) -> [URL]? {
        with(relativePath: relativePath, perform: files)
    }

    /**

     - Note: This function requires a security scope for the content path
     */
    func decodeJsonFiles<T>(in relativeFolder: String) -> [String : T]? where T: Decodable {
        with(relativePath: relativeFolder) { folder in
            guard let files = files(in: folder) else {
                return nil
            }
            return files.filter { $0.pathExtension.lowercased() == "json" }
                .reduce(into: [:]) { items, url in
                    let id = url.deletingPathExtension().lastPathComponent
                    let data: Data
                    do {
                        data = try Data(contentsOf: url)
                    } catch {
                        reportError("Failed to read \(url.path()): \(error)")
                        return
                    }
                    do {
                        items[id] = try decoder.decode(T.self, from: data)
                    } catch {
                        reportError("Failed to decode \(url.path()): \(error)")
                        return
                    }
                }
        }
    }

    // MARK: Generic operations

    func with(relativePath: String, perform operation: (URL) -> Bool) -> Bool {
        perform { operation($0.appending(path: relativePath.withLeadingSlashRemoved)) }
    }

    func with<T>(relativePath: String, perform operation: (URL) -> T?) -> T? {
        perform { operation($0.appending(path: relativePath.withLeadingSlashRemoved)) }
    }

    func with<T>(relativePath: String, perform operation: (URL) async -> T?) async -> T? {
        let path = url.appending(path: relativePath.withLeadingSlashRemoved)
        guard url.startAccessingSecurityScopedResource() else {
            reportError("Failed to start security scope")
            return nil
        }
        defer { url.stopAccessingSecurityScopedResource() }
        return await operation(path)
    }

    /**
     Run an operation in the security scope of a url.
     */
    func perform(_ operation: (URL) -> Bool) -> Bool {
        guard url.startAccessingSecurityScopedResource() else {
            reportError("Failed to start security scope")
            return false
        }
        defer { url.stopAccessingSecurityScopedResource() }
        return operation(url)
    }

    /**
     Run an operation in the content folder
     */
    func perform<T>(_ operation: (URL) -> T?) -> T? {
        guard url.startAccessingSecurityScopedResource() else {
            reportError("Failed to start security scope")
            return nil
        }
        defer { url.stopAccessingSecurityScopedResource() }
        return operation(url)
    }

    func getAllFiles() -> Set<String> {
        guard url.startAccessingSecurityScopedResource() else {
            reportError("Failed to start security scope")
            return []
        }
        guard let enumerator = FileManager.default.enumerator(at: url, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles]) else {
            reportError("Failed to get folder enumerator")
            return []
        }

        var relativePaths = Set<String>()
        let prefix = url.path().withTrailingSlash

        for case let fileURL as URL in enumerator {
            guard !fileURL.hasDirectoryPath else {
                continue
            }
            let fullPath = fileURL.path()
            guard fullPath.hasPrefix(prefix) else {
                print("Expected prefix \(prefix) for \(fullPath)")
                return []
            }
            let relativePath = fullPath.replacingOccurrences(of: prefix, with: "")
            relativePaths.insert(relativePath)
        }

        url.stopAccessingSecurityScopedResource()
        return relativePaths
    }

    // MARK: Unscoped helpers

    private func create(folder: URL) -> Bool {
        do {
            try createIfNeeded(folder)
            return true
        } catch {
            reportError("Failed to create folder \(folder.path())")
            return false
        }
    }

    private func exists(_ url: URL) -> Bool {
        fm.fileExists(atPath: url.path())
    }

    private func remove(_ file: URL) -> Bool {
        guard exists(url) else {
            return true
        }
        do {
            try fm.removeItem(at: file)
        } catch {
            reportError("Failed to delete \(file.path()): \(error)")
            return false
        }
        return true
    }

    private func createParentIfNeeded(of file: URL) throws {
        try createIfNeeded(file.deletingLastPathComponent())
    }

    private func createIfNeeded(_ folder: URL) throws {
        if !exists(folder) {
            try fm.createDirectory(at: folder, withIntermediateDirectories: true)
        }
    }

    private func files(in folder: URL) throws -> [URL] {
        try FileManager.default.contentsOfDirectory(at: folder, includingPropertiesForKeys: nil)
            .filter { !$0.hasDirectoryPath }
    }


    private func files(in folder: URL) -> [URL]? {
        do {
            return try files(in: folder).filter { !$0.lastPathComponent.hasPrefix(".") }
        } catch {
            reportError("Failed to read list of files in \(folder.path())")
            return nil
        }
    }
}
