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

import Foundation

struct SourceContentIndex {
    let rawItems: [MinecraftContentItem]
    let logicalPacks: [LogicalPack]
    let logicalWorlds: [LogicalWorld]
    let packInstances: [PackInstance]
    let worldPackRelationships: [WorldPackRelationship]
    let displayItems: [MinecraftContentItem]
    let displayItemCountsByType: [MinecraftContentType: Int]
    let displayItemCountsByKind: [MinecraftContentKind: Int]
}

enum SourceContentIndexer {
    static func buildIndex(for source: MinecraftSource) -> SourceContentIndex {
        let rawItems = source.rawItems.sorted(by: WorldScanner.sortItems)
        let rawItemsByID = Dictionary(uniqueKeysWithValues: rawItems.map { ($0.id, $0) })

        let rawPacks = rawItems.filter {
            $0.contentType == .behaviorPack || $0.contentType == .resourcePack
        }
        let rawWorlds = rawItems.filter { $0.contentType == .world }

        let packMetadataByItemID = Dictionary(uniqueKeysWithValues: rawPacks.map { item in
            (item.id, packMetadata(for: item, sourceRootURL: source.folderURL))
        })

        var chosenRepresentativeByIdentity: [PackIdentity: MinecraftContentItem] = [:]
        var allPackItemsByIdentity: [PackIdentity: [MinecraftContentItem]] = [:]

        for item in rawPacks {
            let metadata = packMetadataByItemID[item.id] ?? packMetadata(for: item, sourceRootURL: source.folderURL)
            let identity = metadata.identity
            allPackItemsByIdentity[identity, default: []].append(item)

            guard let existing = chosenRepresentativeByIdentity[identity] else {
                chosenRepresentativeByIdentity[identity] = item
                continue
            }

            if shouldPreferPackItem(item, over: existing) {
                chosenRepresentativeByIdentity[identity] = item
            }
        }

        let logicalPacks = allPackItemsByIdentity.keys.sorted {
            let lhs = chosenRepresentativeByIdentity[$0]?.displayName ?? ""
            let rhs = chosenRepresentativeByIdentity[$1]?.displayName ?? ""
            let nameOrder = lhs.localizedStandardCompare(rhs)
            if nameOrder != .orderedSame {
                return nameOrder == .orderedAscending
            }

            return $0.id.localizedStandardCompare($1.id) == .orderedAscending
        }.compactMap { identity -> LogicalPack? in
            guard
                let representativeItem = chosenRepresentativeByIdentity[identity],
                let instances = allPackItemsByIdentity[identity]
            else {
                return nil
            }

            let metadata = packMetadataByItemID[representativeItem.id]

            return LogicalPack(
                id: identity,
                contentType: identity.type,
                displayName: representativeItem.displayName,
                uuid: metadata?.uuid,
                version: metadata?.version,
                representativeItemID: representativeItem.id,
                instanceItemIDs: instances.map(\.id).sorted {
                    $0.path.localizedStandardCompare($1.path) == .orderedAscending
                },
                isSuspicious: identity.isSuspicious
            )
        }

        var packInstances: [PackInstance] = []
        for logicalPack in logicalPacks {
            for itemID in logicalPack.instanceItemIDs {
                guard let item = rawItemsByID[itemID] else {
                    continue
                }

                packInstances.append(
                    PackInstance(
                        id: item.id,
                        itemID: item.id,
                        sourceID: source.id,
                        logicalPackID: logicalPack.id,
                        origin: packOrigin(for: item),
                        hostWorldItemID: hostWorldItemID(for: item, in: rawWorlds)
                    )
                )
            }
        }

        let logicalPacksByID = Dictionary(uniqueKeysWithValues: logicalPacks.map { ($0.id, $0) })
        var worldRelationships: [WorldPackRelationship] = []
        var logicalWorlds: [LogicalWorld] = []

        for world in rawWorlds {
            var usedPackIDs = Set<PackIdentity>()
            var unresolvedReferences: [ContentPackReference] = []

            for reference in world.packReferences {
                let referenceIdentity = PackIdentity(
                    type: reference.type,
                    uuid: reference.uuid,
                    version: reference.version,
                    fallbackName: reference.name,
                    fallbackLocationHint: world.folderName
                )
                let resolvedID = logicalPacksByID[referenceIdentity]?.id

                if let resolvedID {
                    usedPackIDs.insert(resolvedID)
                } else {
                    unresolvedReferences.append(reference)
                }

                worldRelationships.append(
                    WorldPackRelationship(
                        worldItemID: world.id,
                        logicalPackID: resolvedID,
                        reference: reference
                    )
                )
            }

            logicalWorlds.append(
                LogicalWorld(
                    id: world.id,
                    itemID: world.id,
                    usedPackIDs: usedPackIDs.sorted {
                        $0.id.localizedStandardCompare($1.id) == .orderedAscending
                    },
                    unresolvedReferences: unresolvedReferences
                )
            )
        }

        let sortedLogicalWorlds = logicalWorlds.sorted {
            guard
                let lhs = rawItemsByID[$0.itemID],
                let rhs = rawItemsByID[$1.itemID]
            else {
                return $0.itemID.path.localizedStandardCompare($1.itemID.path) == .orderedAscending
            }

            return WorldScanner.sortItems(lhs, rhs)
        }

        let sortedPackInstances = packInstances.sorted {
            $0.itemID.path.localizedStandardCompare($1.itemID.path) == .orderedAscending
        }

        let displayItems = buildDisplayItems(
            from: rawItems,
            logicalPacks: logicalPacks,
            rawItemsByID: rawItemsByID
        )

        return SourceContentIndex(
            rawItems: rawItems,
            logicalPacks: logicalPacks,
            logicalWorlds: sortedLogicalWorlds,
            packInstances: sortedPackInstances,
            worldPackRelationships: worldRelationships,
            displayItems: displayItems,
            displayItemCountsByType: displayItemCounts(for: displayItems),
            displayItemCountsByKind: displayItemKindCounts(for: displayItems)
        )
    }

    private static func buildDisplayItems(
        from rawItems: [MinecraftContentItem],
        logicalPacks: [LogicalPack],
        rawItemsByID: [URL: MinecraftContentItem]
    ) -> [MinecraftContentItem] {
        var normalizedItemIDs = Set<URL>()
        var normalizedItems: [MinecraftContentItem] = []

        for item in rawItems where item.contentType == .world {
            guard normalizedItemIDs.insert(item.id).inserted else {
                continue
            }

            normalizedItems.append(item)
        }

        for logicalPack in logicalPacks {
            guard
                let item = rawItemsByID[logicalPack.representativeItemID],
                normalizedItemIDs.insert(item.id).inserted
            else {
                continue
            }

            normalizedItems.append(item)
        }

        for item in rawItems where item.contentType == .skinPack || item.contentType == .worldTemplate {
            guard normalizedItemIDs.insert(item.id).inserted else {
                continue
            }

            normalizedItems.append(item)
        }

        return normalizedItems
    }

    private static func displayItemCounts(for items: [MinecraftContentItem]) -> [MinecraftContentType: Int] {
        items.reduce(into: [MinecraftContentType: Int]()) { counts, item in
            counts[item.contentType, default: 0] += 1
        }
    }

    private static func displayItemKindCounts(for items: [MinecraftContentItem]) -> [MinecraftContentKind: Int] {
        items.reduce(into: [MinecraftContentKind: Int]()) { counts, item in
            counts[item.contentKind, default: 0] += 1
        }
    }

    private static func shouldPreferPackItem(_ candidate: MinecraftContentItem, over existing: MinecraftContentItem) -> Bool {
        let candidateEmbedded = isEmbeddedWorldPack(candidate)
        let existingEmbedded = isEmbeddedWorldPack(existing)

        if candidateEmbedded != existingEmbedded {
            return !candidateEmbedded
        }

        if candidate.metadataLoaded != existing.metadataLoaded {
            return candidate.metadataLoaded
        }

        if (candidate.iconURL != nil) != (existing.iconURL != nil) {
            return candidate.iconURL != nil
        }

        if candidate.previewLoaded != existing.previewLoaded {
            return candidate.previewLoaded
        }

        if candidate.modifiedDate != existing.modifiedDate {
            return (candidate.modifiedDate ?? .distantPast) > (existing.modifiedDate ?? .distantPast)
        }

        return candidate.folderURL.path.localizedStandardCompare(existing.folderURL.path) == .orderedAscending
    }

    private static func packOrigin(for item: MinecraftContentItem) -> PackSource {
        isEmbeddedWorldPack(item) ? .embeddedInWorld : .foundInCollection
    }

    private static func isEmbeddedWorldPack(_ item: MinecraftContentItem) -> Bool {
        item.folderURL.pathComponents.contains(MinecraftContentType.world.collectionFolderName)
    }

    private static func hostWorldItemID(for packItem: MinecraftContentItem, in rawWorlds: [MinecraftContentItem]) -> URL? {
        rawWorlds.first(where: { world in
            packItem.folderURL.path.hasPrefix(world.folderURL.path + "/")
        })?.id
    }

    private static func packMetadata(for item: MinecraftContentItem, sourceRootURL: URL) -> IndexedPackMetadata {
        let uuid = item.packUUID
        let version = item.packVersion

        return IndexedPackMetadata(
            uuid: uuid,
            version: version,
            identity: PackIdentity(
                type: item.contentType,
                uuid: uuid,
                version: version,
                fallbackName: item.displayName,
                fallbackLocationHint: relativePathHint(for: item, sourceRootURL: sourceRootURL)
            )
        )
    }

    private static func relativePathHint(for item: MinecraftContentItem, sourceRootURL: URL) -> String {
        item.folderURL.path.replacingOccurrences(of: sourceRootURL.path + "/", with: "")
    }
}

private struct IndexedPackMetadata {
    let uuid: String?
    let version: String?
    let identity: PackIdentity
}
