import Foundation
import UniformTypeIdentifiers

@MainActor
enum ShichiZipQuickActionInputLoader {
    private enum LoadedFileReference {
        case durable(URL)
        case temporary(URL)
    }

    static func fileURLs(from context: NSExtensionContext,
                         action: ShichiZipQuickAction) async throws -> [URL]
    {
        let logger = ShichiZipQuickActionLogger(action: action)
        let extensionItems = context.inputItems.compactMap { $0 as? NSExtensionItem }
        let itemProviders = extensionItems.flatMap { $0.attachments ?? [] }

        for (index, item) in extensionItems.enumerated() {
            let attachmentCount = item.attachments?.count ?? 0
            let userInfoKeys = item.userInfo?.keys.map { String(describing: $0) }.joined(separator: ", ") ?? ""
            let contentLength = item.attributedContentText?.length ?? 0
            logger.log("inputItem[\(index)] attachments=\(attachmentCount) attributedTextLength=\(contentLength) userInfoKeys=[\(userInfoKeys)]")
        }

        guard !itemProviders.isEmpty else {
            logger.log("no item providers in extension context")
            throw ShichiZipQuickActionError.unsupportedSelection("No files were provided to the Quick Action.")
        }

        var urls: [URL] = []
        for (index, itemProvider) in itemProviders.enumerated() {
            logger.log("provider[\(index)] registeredTypeIdentifiers=\(itemProvider.registeredTypeIdentifiers.joined(separator: ", "))")
            try await urls.append(loadFileURL(from: itemProvider,
                                              action: action))
        }

        return urls.map(\.standardizedFileURL)
    }

    private static func loadFileURL(from itemProvider: NSItemProvider,
                                    action: ShichiZipQuickAction) async throws -> URL
    {
        let logger = ShichiZipQuickActionLogger(action: action)
        if let objectURL = try await loadURLObject(from: itemProvider,
                                                   logger: logger)
        {
            return objectURL
        }

        var firstError: Error?
        var temporaryRepresentationURL: URL?
        for typeIdentifier in candidateTypeIdentifiers(for: itemProvider) {
            do {
                if let fileReference = try await loadInPlaceFileURL(from: itemProvider,
                                                                    typeIdentifier: typeIdentifier)
                {
                    switch fileReference {
                    case let .durable(url):
                        return url
                    case let .temporary(url):
                        temporaryRepresentationURL = temporaryRepresentationURL ?? url
                    }
                }
            } catch {
                logger.log("loadInPlace failed for type=\(typeIdentifier) error=\(String(describing: error))")
                firstError = firstError ?? error
            }

            do {
                if let fileReference = try await loadFileURLRepresentation(from: itemProvider,
                                                                           typeIdentifier: typeIdentifier),
                    case let .temporary(url) = fileReference
                {
                    temporaryRepresentationURL = temporaryRepresentationURL ?? url
                }
            } catch {
                logger.log("loadFileRepresentation failed for type=\(typeIdentifier) error=\(String(describing: error))")
                firstError = firstError ?? error
            }

            do {
                if let itemURL = try await loadItemFileURL(from: itemProvider,
                                                           typeIdentifier: typeIdentifier,
                                                           logger: logger)
                {
                    return itemURL
                }
            } catch {
                logger.log("loadItem failed for type=\(typeIdentifier) error=\(String(describing: error))")
                firstError = firstError ?? error
            }

            do {
                if let dataURL = try await loadDataFileURL(from: itemProvider,
                                                           typeIdentifier: typeIdentifier,
                                                           logger: logger)
                {
                    return dataURL
                }
            } catch {
                logger.log("loadDataRepresentation failed for type=\(typeIdentifier) error=\(String(describing: error))")
                firstError = firstError ?? error
            }
        }

        if let temporaryRepresentationURL {
            logger.log("rejecting temporary file representation path=\(temporaryRepresentationURL.path)")
            throw ShichiZipQuickActionError.temporaryRepresentationUnsupported(action)
        }

        throw firstError ?? ShichiZipQuickActionError.invalidPayload
    }

    private static func loadURLObject(from itemProvider: NSItemProvider,
                                      logger: ShichiZipQuickActionLogger) async throws -> URL?
    {
        guard itemProvider.canLoadObject(ofClass: NSURL.self) else {
            return nil
        }

        return try await withCheckedThrowingContinuation { continuation in
            itemProvider.loadObject(ofClass: NSURL.self) { object, error in
                if let error {
                    continuation.resume(throwing: error)
                    return
                }

                guard let url = object as? URL else {
                    logger.log("loadObject returned non-URL object type=\(String(describing: object.map { type(of: $0) }))")
                    continuation.resume(returning: nil)
                    return
                }

                continuation.resume(returning: url.standardizedFileURL)
            }
        }
    }

    private static func candidateTypeIdentifiers(for itemProvider: NSItemProvider) -> [String] {
        var identifiers = itemProvider.registeredTypeIdentifiers

        if itemProvider.hasItemConformingToTypeIdentifier(UTType.fileURL.identifier),
           !identifiers.contains(UTType.fileURL.identifier)
        {
            identifiers.insert(UTType.fileURL.identifier, at: 0)
        }

        return identifiers
    }

    private static func loadInPlaceFileURL(from itemProvider: NSItemProvider,
                                           typeIdentifier: String) async throws -> LoadedFileReference?
    {
        try await withCheckedThrowingContinuation { continuation in
            itemProvider.loadInPlaceFileRepresentation(forTypeIdentifier: typeIdentifier) { url, isInPlace, error in
                if let error {
                    continuation.resume(throwing: error)
                    return
                }

                guard let url else {
                    continuation.resume(returning: nil)
                    return
                }

                let standardizedURL = url.standardizedFileURL
                continuation.resume(returning: isInPlace ? .durable(standardizedURL) : .temporary(standardizedURL))
            }
        }
    }

    private static func loadFileURLRepresentation(from itemProvider: NSItemProvider,
                                                  typeIdentifier: String) async throws -> LoadedFileReference?
    {
        try await withCheckedThrowingContinuation { continuation in
            itemProvider.loadFileRepresentation(forTypeIdentifier: typeIdentifier) { url, error in
                if let error {
                    continuation.resume(throwing: error)
                    return
                }

                continuation.resume(returning: url.map { .temporary($0.standardizedFileURL) })
            }
        }
    }

    private static func loadItemFileURL(from itemProvider: NSItemProvider,
                                        typeIdentifier: String,
                                        logger: ShichiZipQuickActionLogger) async throws -> URL?
    {
        try await withCheckedThrowingContinuation { continuation in
            itemProvider.loadItem(forTypeIdentifier: typeIdentifier, options: nil) { item, error in
                if let error {
                    continuation.resume(throwing: error)
                    return
                }

                do {
                    let url = try parseFileURL(from: item)
                    continuation.resume(returning: url.standardizedFileURL)
                } catch {
                    let itemTypeDescription = item.map { String(describing: type(of: $0)) } ?? "nil"
                    logger.log("loadItem returned unparseable item for type=\(typeIdentifier) itemType=\(itemTypeDescription)")
                    continuation.resume(returning: nil)
                }
            }
        }
    }

    private static func loadDataFileURL(from itemProvider: NSItemProvider,
                                        typeIdentifier: String,
                                        logger: ShichiZipQuickActionLogger) async throws -> URL?
    {
        try await withCheckedThrowingContinuation { continuation in
            itemProvider.loadDataRepresentation(forTypeIdentifier: typeIdentifier) { data, error in
                if let error {
                    continuation.resume(throwing: error)
                    return
                }

                do {
                    let url = try parseFileURL(from: data)
                    continuation.resume(returning: url.standardizedFileURL)
                } catch {
                    let byteCount = data?.count ?? 0
                    logger.log("loadDataRepresentation returned unparseable data for type=\(typeIdentifier) bytes=\(byteCount)")
                    continuation.resume(returning: nil)
                }
            }
        }
    }

    private nonisolated static func parseFileURL(from item: Any?) throws -> URL {
        if let url = item as? URL {
            return url
        }

        if let nsURL = item as? NSURL {
            return nsURL as URL
        }

        if let string = item as? String {
            if let url = URL(string: string), url.isFileURL {
                return url
            }

            return URL(fileURLWithPath: string)
        }

        if let array = item as? [Any] {
            for candidate in array {
                if let url = try? parseFileURL(from: candidate) {
                    return url
                }
            }
        }

        if let dictionary = item as? [AnyHashable: Any] {
            for value in dictionary.values {
                if let url = try? parseFileURL(from: value) {
                    return url
                }
            }
        }

        if let data = item as? Data,
           let url = try NSKeyedUnarchiver.unarchivedObject(ofClass: NSURL.self, from: data) as URL?
        {
            return url
        }

        if let data = item as? Data {
            var isStale = false
            if let bookmarkURL = try? URL(resolvingBookmarkData: data,
                                          options: [.withoutUI, .withoutMounting],
                                          relativeTo: nil,
                                          bookmarkDataIsStale: &isStale)
            {
                return bookmarkURL
            }

            if let string = String(data: data, encoding: .utf8) {
                return try parseFileURL(from: string)
            }
        }

        throw ShichiZipQuickActionError.invalidPayload
    }
}
