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

import Foundation

enum MinecraftPackageInspector {
    struct InspectionResult: Sendable {
        let archiveURL: URL
        let extractedRootURL: URL
        let contentRootURL: URL
        let contentType: MinecraftContentType
        let displayName: String
        let iconURL: URL?
        let worldMetadata: WorldMetadata?
        let manifestMetadata: MinecraftManifestMetadata?
    }

    enum InspectionError: LocalizedError {
        case unsupportedFileType(String)
        case invalidArchiveLayout

        var errorDescription: String? {
            switch self {
            case .unsupportedFileType(let pathExtension):
                return "Unsupported Minecraft package type: .\(pathExtension)"
            case .invalidArchiveLayout:
                return "The Minecraft package did not contain a valid world or pack layout."
            }
        }
    }

    nonisolated static let supportedPathExtensions: Set<String> = [
        "mcaddon",
        "mcpack",
        "mctemplate",
        "mcworld"
    ]

    nonisolated static func inspectArchive(at archiveURL: URL) throws -> InspectionResult {
        let normalizedArchiveURL = archiveURL.standardizedFileURL
        let pathExtension = normalizedArchiveURL.pathExtension.lowercased()
        guard supportedPathExtensions.contains(pathExtension) else {
            throw InspectionError.unsupportedFileType(pathExtension)
        }

        let fileManager = FileManager.default
        let extractionDirectoryURL = fileManager.temporaryDirectory
            .appendingPathComponent("MinecraftPackageInspection", isDirectory: true)
            .appendingPathComponent(UUID().uuidString, isDirectory: true)

        try fileManager.createDirectory(at: extractionDirectoryURL, withIntermediateDirectories: true)

        do {
            let archive = try ZipArchiveReader(url: normalizedArchiveURL)
            let contentRootURL = try materializedContentRootURL(
                for: archive,
                archivePathExtension: pathExtension,
                in: extractionDirectoryURL,
                fileManager: fileManager
            )
            let contentType = try resolvedContentType(
                forArchivePathExtension: pathExtension,
                contentRootURL: contentRootURL,
                fileManager: fileManager
            )
            let displayName = MinecraftContentMetadataReader.displayName(
                for: contentRootURL,
                contentType: contentType,
                fallbackName: normalizedArchiveURL.deletingPathExtension().lastPathComponent,
                fileManager: fileManager
            )
            let worldMetadata: WorldMetadata? =
                contentType == .world ? MinecraftContentMetadataReader.worldMetadata(in: contentRootURL, fileManager: fileManager) : nil
            let manifestMetadata = MinecraftContentMetadataReader.manifestMetadata(in: contentRootURL, fileManager: fileManager)
            let iconURL = MinecraftContentMetadataReader.iconURL(
                for: contentRootURL,
                contentType: contentType,
                fileManager: fileManager
            )

            return InspectionResult(
                archiveURL: normalizedArchiveURL,
                extractedRootURL: extractionDirectoryURL,
                contentRootURL: contentRootURL,
                contentType: contentType,
                displayName: displayName,
                iconURL: iconURL,
                worldMetadata: worldMetadata,
                manifestMetadata: manifestMetadata
            )
        } catch {
            try? fileManager.removeItem(at: extractionDirectoryURL)
            throw error
        }
    }

    nonisolated static func cleanup(_ result: InspectionResult) {
        try? FileManager.default.removeItem(at: result.extractedRootURL)
    }

    nonisolated private static func resolvedContentType(
        forArchivePathExtension pathExtension: String,
        contentRootURL: URL,
        fileManager: FileManager
    ) throws -> MinecraftContentType {
        switch pathExtension {
        case "mcworld":
            return .world
        case "mctemplate":
            return .worldTemplate
        case "mcpack", "mcaddon":
            return MinecraftContentMetadataReader.inferredPackContentType(for: contentRootURL, fileManager: fileManager)
        default:
            throw InspectionError.unsupportedFileType(pathExtension)
        }
    }

    nonisolated private static func materializedContentRootURL(
        for archive: ZipArchiveReader,
        archivePathExtension: String,
        in extractionDirectoryURL: URL,
        fileManager: FileManager
    ) throws -> URL {
        let contentRootPath = try resolvedContentRootPath(
            in: archive.entries,
            archivePathExtension: archivePathExtension
        )
        let contentRootURL = extractionDirectoryURL.appendingPathComponent(contentRootPath, isDirectory: true)
        try fileManager.createDirectory(at: contentRootURL, withIntermediateDirectories: true)

        let requiredRelativePaths = requiredMetadataRelativePaths(
            in: archive.entries,
            contentRootPath: contentRootPath
        )

        for relativePath in requiredRelativePaths {
            let fullPath = contentRootPath.isEmpty ? relativePath : "\(contentRootPath)/\(relativePath)"
            guard let entry = archive.entry(named: fullPath) else {
                continue
            }

            let destinationURL = contentRootURL.appendingPathComponent(relativePath)
            try fileManager.createDirectory(at: destinationURL.deletingLastPathComponent(), withIntermediateDirectories: true)
            try archive.extract(entry).write(to: destinationURL)
        }

        return contentRootURL
    }

    nonisolated private static func resolvedContentRootPath(
        in entries: [ZipArchiveEntry],
        archivePathExtension: String
    ) throws -> String {
        let filePaths = entries
            .filter { !$0.isDirectory }
            .map(\.path)

        if containsContentMarkers(in: filePaths, prefix: "") {
            return ""
        }

        let rootCandidates = Set(
            filePaths.compactMap { path -> String? in
                guard let firstComponent = path.split(separator: "/").first else {
                    return nil
                }
                return String(firstComponent)
            }
        ).sorted()

        let matchingRoots = rootCandidates.filter { containsContentMarkers(in: filePaths, prefix: $0) }
        guard matchingRoots.count == 1 else {
            throw InspectionError.invalidArchiveLayout
        }

        let root = matchingRoots[0]
        if archivePathExtension == "mcaddon" {
            return root
        }

        return root
    }

    nonisolated private static func containsContentMarkers(in filePaths: [String], prefix: String) -> Bool {
        let normalizedPrefix = prefix.isEmpty ? "" : prefix + "/"

        let worldMarkers = ["level.dat", "levelname.txt"]
        let packMarkers = ["manifest.json", "pack_icon.png", "pack_icon.jpeg", "pack_icon.jpg"]

        if worldMarkers.contains(where: { filePaths.contains(normalizedPrefix + $0) }) {
            return true
        }

        if filePaths.contains(normalizedPrefix + "db") || filePaths.contains(where: { $0.hasPrefix(normalizedPrefix + "db/") }) {
            return true
        }

        return packMarkers.contains(where: { filePaths.contains(normalizedPrefix + $0) })
    }

    nonisolated private static func requiredMetadataRelativePaths(
        in entries: [ZipArchiveEntry],
        contentRootPath: String
    ) -> [String] {
        let fullPrefix = contentRootPath.isEmpty ? "" : contentRootPath + "/"
        let candidateNames = [
            "manifest.json",
            "level.dat",
            "levelname.txt",
            "world_icon.jpeg",
            "world_icon.jpg",
            "world_icon.png",
            "pack_icon.png",
            "pack_icon.jpeg",
            "pack_icon.jpg"
        ]

        let availablePaths = Set(entries.filter { !$0.isDirectory }.map(\.path))
        return candidateNames.filter { availablePaths.contains(fullPrefix + $0) }
    }

    nonisolated private static func looksLikeMinecraftContentRoot(
        _ directoryURL: URL,
        fileManager: FileManager
    ) -> Bool {
        let worldMarkers = [
            "level.dat",
            "levelname.txt"
        ]
        let packMarkers = [
            "manifest.json",
            "pack_icon.png",
            "pack_icon.jpeg",
            "pack_icon.jpg"
        ]

        if worldMarkers.contains(where: { fileManager.fileExists(atPath: directoryURL.appendingPathComponent($0).path) }) {
            return true
        }

        if fileManager.fileExists(atPath: directoryURL.appendingPathComponent("db", isDirectory: true).path) {
            return true
        }

        if packMarkers.contains(where: { fileManager.fileExists(atPath: directoryURL.appendingPathComponent($0).path) }) {
            return true
        }

        return false
    }
}
