// SPDX-FileCopyrightText: 2026 John Burwell and contributors
// SPDX-License-Identifier: AGPL-3.0-or-later

import Foundation

typealias LocalFolderSourceAccess = BedrockLocalFolderSourceAccess

struct BedrockLocalFolderSourceAccess: SourceAccessMethod {
    nonisolated let accessorIdentifier: SourceAccessorIdentifier = "local-folder"

    nonisolated init() {}

    nonisolated func probeLocalFolder(_ url: URL) async -> SourceProbeResult? {
        BedrockContentScanner.probeLocalFolder(url, providerID: accessorIdentifier)
    }

    nonisolated func accessDescriptor(for source: MinecraftSource) -> SourceAccessDescriptor {
        _ = source
        return SourceAccessDescriptor(
            accessorIdentifier: accessorIdentifier,
            kind: .localFolder,
            refreshStrategy: .eagerFullScan
        )
    }

    nonisolated func availability(for source: MinecraftSource) async -> SourceAvailability {
        await accessStatus(for: source).availability
    }

    nonisolated func accessStatus(for source: MinecraftSource) async -> SourceAccessStatus {
        let candidateURL: URL
        let mode: SourceAccessMode
        if case .localFolder(let bookmarkData) = source.origin,
           let bookmarkData {
            mode = .securityScopedLocalFolder
            var isStale = false
            if let resolvedURL = try? URL(
                resolvingBookmarkData: bookmarkData,
                options: [.withSecurityScope],
                relativeTo: nil,
                bookmarkDataIsStale: &isStale
            ) {
                candidateURL = resolvedURL.standardizedFileURL
            } else {
                candidateURL = source.folderURL
            }
        } else {
            mode = .localFileSystem
            candidateURL = source.folderURL
        }

        let availability: SourceAvailability = FileManager.default.fileExists(atPath: candidateURL.path) ? .available : .unavailable
        return SourceAccessStatus(
            availability: availability,
            mode: mode,
            displayName: source.displayName,
            iconSystemName: "folder",
            statusText: availability == .available ? nil : "Folder unavailable",
            warningText: nil
        )
    }

    nonisolated func capabilities(for source: MinecraftSource) async -> SourceCapabilities {
        _ = source
        return .localFolder
    }

    nonisolated func discoverItems(
        for source: MinecraftSource,
        mode: SourceDiscoveryMode,
        onDiscovered: @escaping @Sendable (MinecraftContentItem) -> Void
    ) async throws {
        guard case .localFolder(let bookmarkData) = source.origin else {
            throw SourceAccessError.accessFailed(
                reason: "No local-folder access method is configured for this source type."
            )
        }

        let resolvedURL: URL
        if let bookmarkData {
            var isStale = false
            guard let bookmarkURL = try? URL(
                resolvingBookmarkData: bookmarkData,
                options: [.withSecurityScope],
                relativeTo: nil,
                bookmarkDataIsStale: &isStale
            ) else {
                throw SourceAccessError.accessFailed(
                    reason: "The saved folder bookmark could not be resolved."
                )
            }

            resolvedURL = bookmarkURL.standardizedFileURL
        } else {
            resolvedURL = source.folderURL
        }

        let accessedSecurityScope = resolvedURL.startAccessingSecurityScopedResource()
        defer {
            if accessedSecurityScope {
                resolvedURL.stopAccessingSecurityScopedResource()
            }
        }

        if case .reconcile = mode,
           let snapshot = source.snapshot {
            try discoverItemsByReconcilingCache(
                for: source,
                snapshot: snapshot,
                resolvedURL: resolvedURL,
                onDiscovered: onDiscovered
            )
            return
        }

        _ = try WorldScanner.discoverItems(in: resolvedURL, onDiscovered: onDiscovered)
    }

    nonisolated func enrich(_ item: MinecraftContentItem, for source: MinecraftSource) async -> MinecraftContentItem {
        _ = source
        return await WorldScanner.enrich(item: item)
    }

    nonisolated func loadSize(for item: MinecraftContentItem, in source: MinecraftSource) async -> MinecraftContentItem {
        _ = source
        return WorldScanner.loadSize(for: item)
    }

    nonisolated func listItemContents(for item: MinecraftContentItem, in source: MinecraftSource) async throws -> [DirectoryEntry] {
        _ = source
        let fileManager = FileManager.default
        let urls = try fileManager.contentsOfDirectory(
            at: item.folderURL,
            includingPropertiesForKeys: [.isDirectoryKey],
            options: [.skipsHiddenFiles]
        )

        return urls
            .map { url in
                let isDirectory = (try? url.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) == true
                return DirectoryEntry(name: url.lastPathComponent, isDirectory: isDirectory)
            }
            .sorted { lhs, rhs in
                if lhs.isDirectory != rhs.isDirectory {
                    return lhs.isDirectory && !rhs.isDirectory
                }

                return lhs.name.localizedStandardCompare(rhs.name) == .orderedAscending
            }
    }

    nonisolated func materializeItem(for item: MinecraftContentItem, in source: MinecraftSource) async throws -> URL {
        _ = source
        return item.folderURL
    }

    nonisolated private func discoverItemsByReconcilingCache(
        for source: MinecraftSource,
        snapshot: SourceSnapshot,
        resolvedURL: URL,
        onDiscovered: @escaping @Sendable (MinecraftContentItem) -> Void
    ) throws {
        let currentCollections = Dictionary(
            uniqueKeysWithValues: WorldScanner.collectionSnapshots(in: resolvedURL).map { ($0.folderName, $0) }
        )
        let previousCollections = Dictionary(
            uniqueKeysWithValues: snapshot.collectionSnapshots.map { ($0.folderName, $0) }
        )

        var changedCollectionNames = Set<String>()
        for type in MinecraftContentType.allCases {
            let collectionName = type.collectionFolderName
            let currentFingerprint = currentCollections[collectionName]?.fingerprint
            let previousFingerprint = previousCollections[collectionName]?.fingerprint
            if currentFingerprint != previousFingerprint {
                changedCollectionNames.insert(collectionName)
            }
        }

        let unchangedCollectionNames = Set(currentCollections.keys).subtracting(changedCollectionNames)
        var reconciledItems = source.rawItems.filter { item in
            guard let collectionName = topLevelCollectionName(for: item, sourceRootURL: resolvedURL) else {
                return false
            }

            return unchangedCollectionNames.contains(collectionName)
        }

        for type in MinecraftContentType.allCases {
            let collectionName = type.collectionFolderName
            guard changedCollectionNames.contains(collectionName) else {
                continue
            }

            let collectionURL = resolvedURL.appendingPathComponent(collectionName, isDirectory: true)
            let discoveredItems = try WorldScanner.discoverItems(
                inCollectionRootURL: collectionURL,
                contentType: type
            )
            reconciledItems.append(contentsOf: discoveredItems)
        }

        reconciledItems.sort(by: WorldScanner.sortItems)
        for item in reconciledItems {
            onDiscovered(item)
        }
    }

    nonisolated private func topLevelCollectionName(for item: MinecraftContentItem, sourceRootURL: URL) -> String? {
        let relativePath = item.folderURL.path.replacingOccurrences(of: sourceRootURL.path + "/", with: "")
        let components = relativePath.split(separator: "/")
        return components.first.map(String.init)
    }
}

struct JavaLocalFolderSourceAccess: SourceAccessMethod {
    nonisolated let accessorIdentifier: SourceAccessorIdentifier = "java-local-folder"
    private let candidateDiscoveryRoots: [URL]?

    nonisolated init(candidateDiscoveryRoots: [URL]? = nil) {
        self.candidateDiscoveryRoots = candidateDiscoveryRoots
    }

    nonisolated func probeLocalFolder(_ url: URL) async -> SourceProbeResult? {
        JavaContentScanner.probeLocalFolder(url, providerID: accessorIdentifier)
    }

    nonisolated func discoverSourceCandidates() -> AsyncThrowingStream<SourceCandidateEvent, Error> {
        AsyncThrowingStream { continuation in
            let roots = candidateDiscoveryRoots
            let providerID = accessorIdentifier
            let task = Task.detached(priority: .utility) {
                continuation.yield(
                    .stageUpdated(
                        WorkStage(
                            id: "\(providerID)-candidate-discovery",
                            title: "Finding Java sources",
                            detail: nil,
                            state: .running,
                            progress: .indeterminate
                        )
                    )
                )

                let candidates = JavaContentScanner.discoverSourceCandidates(
                    providerID: providerID,
                    searchRoots: roots
                )
                for candidate in candidates {
                    continuation.yield(.candidate(candidate))
                }

                continuation.yield(
                    .stageUpdated(
                        WorkStage(
                            id: "\(providerID)-candidate-discovery",
                            title: "Finding Java sources",
                            detail: candidates.isEmpty ? "No Java sources found." : "Found \(candidates.count) Java sources.",
                            state: .succeeded,
                            progress: .indeterminate
                        )
                    )
                )
                continuation.finish()
            }

            continuation.onTermination = { @Sendable _ in
                task.cancel()
            }
        }
    }

    nonisolated func accessDescriptor(for source: MinecraftSource) -> SourceAccessDescriptor {
        _ = source
        return SourceAccessDescriptor(
            accessorIdentifier: accessorIdentifier,
            kind: .localFolder,
            refreshStrategy: .eagerFullScan
        )
    }

    nonisolated func accessStatus(for source: MinecraftSource) async -> SourceAccessStatus {
        let candidateURL: URL
        let mode: SourceAccessMode
        let bookmarkData: Data?
        switch source.origin {
        case .javaLocalFolder(let data), .localFolder(let data):
            bookmarkData = data
        case .connectedDevice:
            bookmarkData = nil
        }

        if let bookmarkData {
            mode = .securityScopedLocalFolder
            var isStale = false
            if let resolvedURL = try? URL(
                resolvingBookmarkData: bookmarkData,
                options: [.withSecurityScope],
                relativeTo: nil,
                bookmarkDataIsStale: &isStale
            ) {
                candidateURL = resolvedURL.standardizedFileURL
            } else {
                candidateURL = source.folderURL
            }
        } else {
            mode = .localFileSystem
            candidateURL = source.folderURL
        }

        let availability: SourceAvailability = FileManager.default.fileExists(atPath: candidateURL.path) ? .available : .unavailable
        return SourceAccessStatus(
            availability: availability,
            mode: mode,
            displayName: source.displayName,
            iconSystemName: "folder",
            statusText: availability == .available ? nil : "Folder unavailable",
            warningText: nil
        )
    }

    nonisolated func capabilities(for source: MinecraftSource) async -> SourceCapabilities {
        _ = source
        return .localFolder
    }

    nonisolated func discoverItems(
        for source: MinecraftSource,
        mode: SourceDiscoveryMode,
        onDiscovered: @escaping @Sendable (MinecraftContentItem) -> Void
    ) async throws {
        _ = mode
        let bookmarkData: Data?
        switch source.origin {
        case .javaLocalFolder(let data), .localFolder(let data):
            bookmarkData = data
        case .connectedDevice:
            throw SourceAccessError.accessFailed(
                reason: "No Java local-folder access method is configured for this source type."
            )
        }

        let resolvedURL: URL
        if let bookmarkData {
            var isStale = false
            guard let bookmarkURL = try? URL(
                resolvingBookmarkData: bookmarkData,
                options: [.withSecurityScope],
                relativeTo: nil,
                bookmarkDataIsStale: &isStale
            ) else {
                throw SourceAccessError.accessFailed(
                    reason: "The saved folder bookmark could not be resolved."
                )
            }

            resolvedURL = bookmarkURL.standardizedFileURL
        } else {
            resolvedURL = source.folderURL
        }

        let accessedSecurityScope = resolvedURL.startAccessingSecurityScopedResource()
        defer {
            if accessedSecurityScope {
                resolvedURL.stopAccessingSecurityScopedResource()
            }
        }

        _ = try JavaContentScanner.discoverItems(in: resolvedURL, onDiscovered: onDiscovered)
    }

    nonisolated func enrich(_ item: MinecraftContentItem, for source: MinecraftSource) async -> MinecraftContentItem {
        _ = source
        return await JavaContentScanner.enrich(item: item)
    }

    nonisolated func loadSize(for item: MinecraftContentItem, in source: MinecraftSource) async -> MinecraftContentItem {
        _ = source
        return JavaContentScanner.loadSize(for: item)
    }

    nonisolated func listItemContents(for item: MinecraftContentItem, in source: MinecraftSource) async throws -> [DirectoryEntry] {
        _ = source
        let values = try? item.folderURL.resourceValues(forKeys: [.isDirectoryKey])
        guard values?.isDirectory == true else {
            return []
        }

        return try await BedrockLocalFolderSourceAccess().listItemContents(for: item, in: source)
    }

    nonisolated func materializeItem(for item: MinecraftContentItem, in source: MinecraftSource) async throws -> URL {
        _ = source
        return item.folderURL
    }
}
