local _, ns = ...
local L = ns.L or {}

local UIData = ns.UIData or {}
local PREMIUM = UIData.PREMIUM or {}
local THEME = UIData.THEME or {}
local STATUS_COLORS = UIData.STATUS_COLORS or {}
local RAID_DIFFICULTY_OPTIONS = UIData.RAID_DIFFICULTY_OPTIONS or {}
local DUNGEON_DIFFICULTY_OPTIONS = UIData.DUNGEON_DIFFICULTY_OPTIONS or {}
local RAID_DIFFICULTY_IDS = UIData.RAID_DIFFICULTY_IDS or {}
local DUNGEON_DIFFICULTY_IDS = UIData.DUNGEON_DIFFICULTY_IDS or {}
local DUNGEON_JOURNAL_SUBTAB_CONFIG = UIData.DUNGEON_JOURNAL_SUBTAB_CONFIG or {}
local MIDNIGHT_RAID_FALLBACK = UIData.MIDNIGHT_RAID_FALLBACK or {}
local MIDNIGHT_DUNGEON_FALLBACK = UIData.MIDNIGHT_DUNGEON_FALLBACK or {}
local SEASONAL_DUNGEON_KEY_GROUPS = UIData.SEASONAL_DUNGEON_KEY_GROUPS or {}
local OFFICIAL_SEASON_ONE_LEGACY_DUNGEON_KEYS = UIData.OFFICIAL_SEASON_ONE_LEGACY_DUNGEON_KEYS or {}
local WISHLIST_CONTENT_SORT_ORDER = UIData.WISHLIST_CONTENT_SORT_ORDER or {}

local JournalView = {}
ns.UIJournalView = JournalView

local function getJournalDifficultyOptions(mainTabKey)
    if mainTabKey == "dungeons" then
        return DUNGEON_DIFFICULTY_OPTIONS
    end

    return RAID_DIFFICULTY_OPTIONS
end

local function usesDungeonDailyCounter(difficultyKey)
    return difficultyKey == "mythic"
end

local function isJournalSubTab(mainTabKey, subTabKey)
    if mainTabKey == "raids" then
        return subTabKey == "midnight"
    end

    if mainTabKey == "dungeons" then
        return DUNGEON_JOURNAL_SUBTAB_CONFIG[subTabKey or ""] ~= nil
    end

    return false
end

local function getJournalModeConfig(mainTabKey, subTabKey)
    if mainTabKey == "dungeons" then
        local dungeonSubTab = DUNGEON_JOURNAL_SUBTAB_CONFIG[subTabKey or "midnight"] or DUNGEON_JOURNAL_SUBTAB_CONFIG.midnight
        return {
            mainTabKey = "dungeons",
            tabLabel = L.DUNGEONS or "Dungeons",
            subTabKey = subTabKey or "midnight",
            subTabLabel = dungeonSubTab.subTabLabel(),
            primaryLabel = L.DUNGEON_JOURNAL_PRIMARY_LABEL or "Dungeon",
            primaryLabelPlural = L.DUNGEONS or "Dungeons",
            bossesLabel = L.RAID_JOURNAL_BOSSES or "Bosses",
            listSubtitle = dungeonSubTab.listSubtitle(),
            fallbackDescription = dungeonSubTab.fallbackDescription(),
            difficultyOptions = DUNGEON_DIFFICULTY_OPTIONS,
            difficultyIDs = DUNGEON_DIFFICULTY_IDS,
            isRaid = false,
        }
    end

    return {
        mainTabKey = "raids",
        subTabKey = "midnight",
        tabLabel = L.RAIDS or "Raids",
        subTabLabel = L.MAIN_TAB_RAIDS_MIDNIGHT or "Midnight",
        primaryLabel = L.RAID_JOURNAL_PRIMARY_LABEL or "Raid",
        primaryLabelPlural = L.RAIDS or "Raids",
        bossesLabel = L.RAID_JOURNAL_BOSSES or "Bosses",
        listSubtitle = L.RAID_JOURNAL_MIDNIGHT_SUBTITLE or "Choose a Midnight raid.",
        fallbackDescription = L.RAID_JOURNAL_FALLBACK_DESCRIPTION or "This first build wires the raid journal layout, state handling, and filters. Live encounter and loot sync can be connected on top of this structure.",
        difficultyOptions = RAID_DIFFICULTY_OPTIONS,
        difficultyIDs = RAID_DIFFICULTY_IDS,
        isRaid = true,
    }
end

local function getRaidDifficultyLabel(difficultyKey, mainTabKey)
    for _, option in ipairs(getJournalDifficultyOptions(mainTabKey)) do
        if option.key == difficultyKey then
            return option.label()
        end
    end

    if mainTabKey == "dungeons" then
        for _, option in ipairs(RAID_DIFFICULTY_OPTIONS) do
            if option.key == difficultyKey then
                return option.label()
            end
        end
    end

    return L.UNKNOWN or "Unknown"
end

local getRaidClassFilterSelection

local function normalizeRaidJournalKey(text)
    local value = string.lower(tostring(text or ""))
    value = value:gsub("[\195\132\195\164]", "a")
    value = value:gsub("[\195\150\195\182]", "o")
    value = value:gsub("[\195\156\195\188]", "u")
    value = value:gsub("\195\159", "ss")
    value = value:gsub("[^%w]+", "-")
    value = value:gsub("%-+", "-")
    value = value:gsub("^%-", "")
    value = value:gsub("%-$", "")
    return value
end

local function sanitizeRaidLootMetaText(text)
    if type(text) ~= "string" then
        return nil
    end

    text = text:gsub("|c%x%x%x%x%x%x%x%x", ""):gsub("|r", "")
    text = text:gsub("^%s+", ""):gsub("%s+$", "")
    if text == "" then
        return nil
    end

    local normalized = normalizeRaidJournalKey(text)
    if normalized == "" then
        return nil
    end

    if normalized == normalizeRaidJournalKey(L.UNKNOWN or "Unknown")
        or normalized == normalizeRaidJournalKey(L.RAID_JOURNAL_ALL_CLASSES or "All classes")
    then
        return nil
    end

    return text
end

local function getRaidLootEquipLabel(itemEquipLoc)
    if type(itemEquipLoc) ~= "string" or itemEquipLoc == "" then
        return nil
    end

    if itemEquipLoc == "INVTYPE_FINGER" or itemEquipLoc == "INVTYPE_TRINKET" then
        return L.RAID_LOOT_META_JEWELRY or "Jewelry"
    end

    return sanitizeRaidLootMetaText(_G[itemEquipLoc])
end

local function isRaidLootDecoration(itemType, itemSubType)
    local haystack = normalizeRaidJournalKey(string.format("%s %s", tostring(itemType or ""), tostring(itemSubType or "")))
    if haystack == "" then
        return false
    end

    return haystack:find("housing", 1, true) ~= nil
        or haystack:find("behaus", 1, true) ~= nil
        or haystack:find("decor", 1, true) ~= nil
        or haystack:find("dekor", 1, true) ~= nil
end

local function buildRaidLootMetaLabel(loot)
    if type(loot) ~= "table" then
        return ""
    end

    local itemType = loot.itemType
    local itemSubType = loot.itemSubType
    local itemEquipLoc = loot.itemEquipLoc
    local classID = loot.classID
    local subClassID = loot.subClassID

    if loot.itemID and C_Item and C_Item.GetItemInfoInstant then
        local _, infoItemType, infoItemSubType, infoEquipLoc, _, infoClassID, infoSubClassID = C_Item.GetItemInfoInstant(loot.itemID)
        itemType = itemType or infoItemType
        itemSubType = itemSubType or infoItemSubType
        itemEquipLoc = itemEquipLoc or infoEquipLoc
        if classID == nil then
            classID = infoClassID
        end
        if subClassID == nil then
            subClassID = infoSubClassID
        end
    end

    local slotLabel = getRaidLootEquipLabel(itemEquipLoc) or sanitizeRaidLootMetaText(loot.slot)
    local subTypeLabel = sanitizeRaidLootMetaText(loot.armorType) or sanitizeRaidLootMetaText(itemSubType)
    local typeLabel = sanitizeRaidLootMetaText(itemType)

    if isRaidLootDecoration(itemType, itemSubType) then
        return L.RAID_LOOT_META_DECORATION or "Decoration"
    end

    if classID == 9 then
        return subTypeLabel or typeLabel or slotLabel or ""
    end

    if itemEquipLoc == "INVTYPE_FINGER" or itemEquipLoc == "INVTYPE_TRINKET" then
        return L.RAID_LOOT_META_JEWELRY or "Jewelry"
    end

    if slotLabel and subTypeLabel then
        if normalizeRaidJournalKey(slotLabel) ~= normalizeRaidJournalKey(subTypeLabel) then
            return slotLabel .. "  " .. subTypeLabel
        end
        return slotLabel
    end

    return slotLabel or subTypeLabel or typeLabel or sanitizeRaidLootMetaText(loot.slot) or ""
end

local function getClassIDFromToken(classToken)
    if type(classToken) ~= "string" or classToken == "" or type(GetClassInfo) ~= "function" then
        return nil
    end

    for classID = 1, 20 do
        local _, token = GetClassInfo(classID)
        if token == classToken then
            return classID
        end
    end

    return nil
end

local function getClassInfoByToken(classToken)
    if type(classToken) ~= "string" or classToken == "" or type(GetClassInfo) ~= "function" then
        return nil
    end

    for classID = 1, 20 do
        local className, token = GetClassInfo(classID)
        if token == classToken then
            return {
                classID = classID,
                className = className,
                classToken = token,
            }
        end
    end

    return nil
end

getRaidClassFilterSelection = function(character, classFilterKey)
    local playerToken = character and (character.classFile or character.classToken or character.classFilename) or nil
    local playerName = character and character.className or (L.UNKNOWN or "Unknown")
    local key = type(classFilterKey) == "string" and classFilterKey or "player"

    if key == "player" then
        return {
            key = "player",
            classID = getClassIDFromToken(playerToken) or 0,
            classToken = playerToken,
            className = playerName,
            displayName = playerName,
            isPlayer = true,
        }
    end

    if key == "all" then
        return {
            key = "all",
            classID = 0,
            classToken = nil,
            className = L.RAID_JOURNAL_ALL_CLASSES or "All classes",
            displayName = L.RAID_JOURNAL_ALL_CLASSES or "All classes",
            isAll = true,
        }
    end

    local classInfo = getClassInfoByToken(key)
    if classInfo then
        return {
            key = key,
            classID = classInfo.classID or 0,
            classToken = classInfo.classToken,
            className = classInfo.className or (L.UNKNOWN or "Unknown"),
            displayName = classInfo.className or (L.UNKNOWN or "Unknown"),
        }
    end

    return getRaidClassFilterSelection(character, "player")
end

local function getRaidLootTrackerVisual(state)
    if state == "wish" then
        return PREMIUM.raidTrackerWish, THEME.gold, L.RAID_LOOT_TRACKER_WISH or "Wishlisted"
    end

    if state == "obtained" then
        return PREMIUM.raidTrackerObtained, STATUS_COLORS.complete, L.RAID_LOOT_TRACKER_OBTAINED or "Obtained"
    end

    return PREMIUM.raidTrackerNone, STATUS_COLORS.missing, L.RAID_LOOT_TRACKER_NONE or "Not tracked"
end

local function buildRaidLootTrackerSummary(characterKey, difficultyKey, lootEntries)
    if type(characterKey) ~= "string" or characterKey == "" then
        return nil
    end

    local wishCount = 0
    local obtainedCount = 0
    local seen = {}

    for _, loot in ipairs(lootEntries or {}) do
        local itemID = loot and tonumber(loot.itemID) or nil
        if itemID and not seen[itemID] then
            seen[itemID] = true
            local trackerState = ns.GetRaidLootTrackerState and ns:GetRaidLootTrackerState(characterKey, difficultyKey, itemID) or nil
            if trackerState == "wish" then
                wishCount = wishCount + 1
            elseif trackerState == "obtained" then
                obtainedCount = obtainedCount + 1
            end
        end
    end

    if wishCount == 0 and obtainedCount == 0 then
        return nil
    end

    return {
        wishCount = wishCount,
        obtainedCount = obtainedCount,
    }
end

local function mergeRaidLootTrackerSummary(target, source)
    if not source then
        return target
    end

    target = target or {
        wishCount = 0,
        obtainedCount = 0,
    }
    target.wishCount = (target.wishCount or 0) + (source.wishCount or 0)
    target.obtainedCount = (target.obtainedCount or 0) + (source.obtainedCount or 0)

    if target.wishCount == 0 and target.obtainedCount == 0 then
        return nil
    end

    return target
end

local function getRaidLootTrackerBadgeVisual(summary)
    if not summary then
        return nil
    end

    if (summary.wishCount or 0) > 0 then
        return PREMIUM.raidTrackerWish
    end

    if (summary.obtainedCount or 0) > 0 then
        return PREMIUM.raidTrackerObtained
    end

    return nil
end

local function addRaidLootTrackerSummaryTooltip(summary)
    if not summary then
        return
    end

    GameTooltip:AddLine(" ")
    GameTooltip:AddLine(L.RAID_LOOT_TRACKER_BADGE or "Tracked loot", THEME.gold[1], THEME.gold[2], THEME.gold[3], true)
    if (summary.wishCount or 0) > 0 then
        GameTooltip:AddLine(
            string.format(L.RAID_LOOT_TRACKER_WISH_COUNT or "Wishlisted: %d", summary.wishCount),
            THEME.gold[1],
            THEME.gold[2],
            THEME.gold[3],
            true
        )
    end
    if (summary.obtainedCount or 0) > 0 then
        GameTooltip:AddLine(
            string.format(L.RAID_LOOT_TRACKER_OBTAINED_COUNT or "Obtained: %d", summary.obtainedCount),
            STATUS_COLORS.complete[1],
            STATUS_COLORS.complete[2],
            STATUS_COLORS.complete[3],
            true
        )
    end
end

local function getWishlistSubTabLabel(mainTabKey, subTabKey)
    if mainTabKey == "raids" then
        return L.MAIN_TAB_RAIDS_MIDNIGHT or "Midnight"
    end

    if mainTabKey == "dungeons" and subTabKey == "season1" then
        return L.MAIN_TAB_DUNGEONS_SEASON1 or "Season 1"
    end

    if mainTabKey == "dungeons" then
        return L.MAIN_TAB_DUNGEONS_MIDNIGHT or "Midnight"
    end

    return nil
end

local function getWishlistSourceLabel(mainTabKey, subTabKey)
    local mainLabel = mainTabKey == "raids"
        and (L.WISHLIST_SOURCE_RAID or "Raid")
        or (L.WISHLIST_SOURCE_DUNGEON or "Dungeon")
    local subTabLabel = getWishlistSubTabLabel(mainTabKey, subTabKey)
    if subTabLabel and subTabLabel ~= "" then
        return string.format("%s / %s", mainLabel, subTabLabel)
    end

    return mainLabel
end

local function buildWishlistSourcePathText(source)
    if type(source) ~= "table" then
        return L.WISHLIST_SOURCE_UNKNOWN or "Source pending"
    end

    local instanceName = tostring(source.instanceName or "")
    local bossName = tostring(source.bossName or "")
    if instanceName ~= "" and bossName ~= "" then
        return string.format("%s -> %s", instanceName, bossName)
    end

    if instanceName ~= "" then
        return instanceName
    end

    if bossName ~= "" then
        return bossName
    end

    return L.WISHLIST_SOURCE_UNKNOWN or "Source pending"
end

local function buildWishlistEntryMeta(entry)
    local source = entry and entry.sources and entry.sources[1] or nil
    local mainTabKey = source and source.mainTabKey or "raids"
    local difficultyKey = entry and entry.difficultyKey or (source and source.difficultyKey) or "normal"
    local metaParts = {
        getWishlistSourceLabel(mainTabKey, source and source.subTabKey),
        buildWishlistSourcePathText(source),
        getRaidDifficultyLabel(difficultyKey, mainTabKey),
    }

    local lootMeta = buildRaidLootMetaLabel(entry and entry.loot or nil)
    if lootMeta and lootMeta ~= "" then
        metaParts[#metaParts + 1] = lootMeta
    end

    if entry and entry.sources and #entry.sources > 1 then
        metaParts[#metaParts + 1] = string.format(L.WISHLIST_MORE_SOURCES or "+%d more", #entry.sources - 1)
    end

    return table.concat(metaParts, "  |  ")
end

local function addWishlistSourcesTooltip(sources)
    if type(sources) ~= "table" or #sources == 0 then
        return
    end

    GameTooltip:AddLine(" ")
    GameTooltip:AddLine(L.WISHLIST_TOOLTIP_SOURCES or "Sources", THEME.gold[1], THEME.gold[2], THEME.gold[3], true)
    for _, source in ipairs(sources) do
        GameTooltip:AddLine(
            string.format(
                "%s  |  %s  |  %s",
                getWishlistSourceLabel(source.mainTabKey, source.subTabKey),
                buildWishlistSourcePathText(source),
                getRaidDifficultyLabel(source.difficultyKey or "normal", source.mainTabKey or "raids")
            ),
            0.86,
            0.86,
            0.82,
            true
        )
    end
end

local function getRaidLootQualityColor(loot)
    local defaultColor = (ITEM_QUALITY_COLORS and ITEM_QUALITY_COLORS.common) or {
        r = THEME.parchment[1],
        g = THEME.parchment[2],
        b = THEME.parchment[3],
    }
    if type(loot) ~= "table" then
        return nil, defaultColor
    end

    local quality
    local itemRef = loot.link or loot.itemID
    if itemRef and type(GetItemInfo) == "function" then
        local _, _, itemQuality = GetItemInfo(itemRef)
        quality = tonumber(itemQuality) or quality
    end

    if (not quality) and loot.itemID and C_Item and C_Item.GetItemQualityByID then
        quality = C_Item.GetItemQualityByID(loot.itemID)
    end

    local color = quality and ITEM_QUALITY_COLORS and ITEM_QUALITY_COLORS[quality] or defaultColor
    return quality, color or defaultColor
end

local function copyRaidJournalTable(source)
    if type(source) ~= "table" then
        return source
    end

    local result = {}
    for key, value in pairs(source) do
        result[key] = copyRaidJournalTable(value)
    end
    return result
end

local function buildCurrentEncounterLockoutLookup(character, isRaidContent, difficultyIDFilter)
    if not character or not character.key or not ns:IsCurrentCharacterKey(character.key) then
        return {}
    end

    if type(GetNumSavedInstances) ~= "function" or type(GetSavedInstanceInfo) ~= "function" or type(GetSavedInstanceEncounterInfo) ~= "function" then
        return {}
    end

    local encounters = {}
    local instanceCount = GetNumSavedInstances() or 0
    for instanceIndex = 1, instanceCount do
        local name, _, _, difficultyID, locked, _, _, isRaid, _, _, encounterTotal, encounterProgress = GetSavedInstanceInfo(instanceIndex)
        if isRaid == (isRaidContent == true)
            and locked
            and (difficultyIDFilter == nil or tonumber(difficultyID) == tonumber(difficultyIDFilter))
            and type(name) == "string"
            and name ~= ""
        then
            local instanceKey = normalizeRaidJournalKey(name)
            local instanceEntry = encounters[instanceKey] or {
                name = name,
                progressText = string.format("%d/%d", tonumber(encounterProgress) or 0, tonumber(encounterTotal) or 0),
                kills = {},
            }

            for encounterIndex = 1, tonumber(encounterTotal) or 0 do
                local bossName, _, isKilled = GetSavedInstanceEncounterInfo(instanceIndex, encounterIndex)
                if type(bossName) == "string" and bossName ~= "" then
                    instanceEntry.kills[normalizeRaidJournalKey(bossName)] = isKilled == true
                end
            end

            encounters[instanceKey] = instanceEntry
        end
    end

    return encounters
end

local function buildEncounterJournalLoot(instanceID, encounterID, difficultyKey, classFilterKey, character, mainTabKey, tierIndex)
    if type(encounterID) ~= "number" or encounterID <= 0 then
        return {}
    end

    if type(EJ_SelectEncounter) ~= "function"
        or type(EJ_GetNumLoot) ~= "function"
        or type(EJ_SetDifficulty) ~= "function"
        or type(EJ_SetLootFilter) ~= "function"
        or type(C_EncounterJournal) ~= "table"
        or type(C_EncounterJournal.GetLootInfoByIndex) ~= "function"
    then
        return {}
    end

    local classFilter = getRaidClassFilterSelection(character, classFilterKey)
    local classID = classFilter.classID or 0
    local journalConfig = getJournalModeConfig(mainTabKey)

    if type(EJ_ClearSearch) == "function" then
        pcall(EJ_ClearSearch)
    end
    if type(EJ_ResetLootFilter) == "function" then
        pcall(EJ_ResetLootFilter)
    end
    if type(C_EncounterJournal) == "table" and type(C_EncounterJournal.ResetSlotFilter) == "function" then
        pcall(C_EncounterJournal.ResetSlotFilter)
    end

    local items = {}
    local seen = {}
    local difficultyIDs
    if difficultyKey == "all" then
        difficultyIDs = {}
        for _, option in ipairs(journalConfig.difficultyOptions or RAID_DIFFICULTY_OPTIONS) do
            local difficultyID = journalConfig.difficultyIDs and journalConfig.difficultyIDs[option.key]
            if difficultyID then
                difficultyIDs[#difficultyIDs + 1] = difficultyID
            end
        end
    else
        difficultyIDs = {
            (journalConfig.difficultyIDs and journalConfig.difficultyIDs[difficultyKey])
                or RAID_DIFFICULTY_IDS[difficultyKey]
                or RAID_DIFFICULTY_IDS.normal
        }
    end

    for _, difficultyID in ipairs(difficultyIDs) do
        if type(EJ_SelectTier) == "function" and type(tierIndex) == "number" and tierIndex > 0 then
            pcall(EJ_SelectTier, tierIndex)
        end
        pcall(EJ_SetDifficulty, difficultyID)
        if type(EJ_SelectInstance) == "function" and type(instanceID) == "number" and instanceID > 0 then
            pcall(EJ_SelectInstance, instanceID)
        end
        pcall(EJ_SetLootFilter, classID, 0)
        if Enum and Enum.ItemSlotFilterType and type(C_EncounterJournal.SetSlotFilter) == "function" then
            pcall(C_EncounterJournal.SetSlotFilter, Enum.ItemSlotFilterType.NoFilter)
        end
        pcall(EJ_SelectEncounter, encounterID)

        local okCount, lootCount = pcall(EJ_GetNumLoot)
        if okCount and tonumber(lootCount) and tonumber(lootCount) > 0 then
            for lootIndex = 1, tonumber(lootCount) do
                local okLoot, itemInfo = pcall(C_EncounterJournal.GetLootInfoByIndex, lootIndex)
                local itemID = okLoot and itemInfo and tonumber(itemInfo.itemID) or nil
                local itemName = okLoot and itemInfo and itemInfo.name or nil
                if okLoot and itemInfo and ((type(itemName) == "string" and itemName ~= "") or itemID) then
                    local dedupeKey = itemID or (tostring(itemName) .. ":" .. tostring(itemInfo.slot))
                    if not seen[dedupeKey] then
                        seen[dedupeKey] = true
                        local itemType
                        local itemSubType
                        local itemEquipLoc
                        local infoClassID
                        local infoSubClassID
                        if itemID and C_Item and C_Item.GetItemInfoInstant then
                            local _, infoItemType, infoItemSubType, infoEquipLoc, _, classIDFromItem, subClassIDFromItem =
                                C_Item.GetItemInfoInstant(itemID)
                            itemType = infoItemType
                            itemSubType = infoItemSubType
                            itemEquipLoc = infoEquipLoc
                            infoClassID = classIDFromItem
                            infoSubClassID = subClassIDFromItem
                        end
                        local slotText = itemInfo.slot
                        local armorText = itemInfo.armorType
                        local restrictionText
                        if classFilter.key ~= "all" then
                            restrictionText = classFilter.className or (L.UNKNOWN or "Unknown")
                        elseif type(armorText) == "string" and armorText ~= "" then
                            restrictionText = armorText
                        else
                            restrictionText = L.RAID_JOURNAL_ALL_CLASSES or "All classes"
                        end
                        items[#items + 1] = {
                            name = (type(itemName) == "string" and itemName ~= "") and itemName or ("Item " .. tostring(itemID)),
                            slot = (type(slotText) == "string" and slotText ~= "") and slotText
                                or ((type(armorText) == "string" and armorText ~= "") and armorText or (L.UNKNOWN or "Unknown")),
                            restriction = restrictionText,
                            icon = itemInfo.icon,
                            itemID = itemID,
                            link = itemInfo.link,
                            itemType = itemType,
                            itemSubType = itemSubType,
                            itemEquipLoc = itemEquipLoc,
                            classID = infoClassID,
                            subClassID = infoSubClassID,
                            armorType = armorText,
                            isVeryRare = itemInfo.displayAsVeryRare,
                            isExtremelyRare = itemInfo.displayAsExtremelyRare,
                            isPerPlayerLoot = itemInfo.displayAsPerPlayerLoot,
                        }
                    end
                end
            end
        end
    end

    return items
end

local function buildEncounterJournalLootEncounterMap(instanceID, tierIndex, difficultyID)
    if type(instanceID) ~= "number" or instanceID <= 0 then
        return {}
    end

    if type(EJ_GetNumLoot) ~= "function"
        or type(EJ_SelectInstance) ~= "function"
        or type(C_EncounterJournal) ~= "table"
        or type(C_EncounterJournal.GetLootInfoByIndex) ~= "function"
    then
        return {}
    end

    if type(EJ_ClearSearch) == "function" then
        pcall(EJ_ClearSearch)
    end
    if type(EJ_ResetLootFilter) == "function" then
        pcall(EJ_ResetLootFilter)
    end
    if type(C_EncounterJournal.ResetSlotFilter) == "function" then
        pcall(C_EncounterJournal.ResetSlotFilter)
    end
    if type(EJ_SelectTier) == "function" and type(tierIndex) == "number" and tierIndex > 0 then
        pcall(EJ_SelectTier, tierIndex)
    end
    if type(EJ_SetDifficulty) == "function" and type(difficultyID) == "number" and difficultyID > 0 then
        pcall(EJ_SetDifficulty, difficultyID)
    end
    pcall(EJ_SelectInstance, instanceID)

    local encounters = {}
    local numLoot = EJ_GetNumLoot() or 0
    for lootIndex = 1, numLoot do
        local okLoot, itemInfo = pcall(C_EncounterJournal.GetLootInfoByIndex, lootIndex)
        local encounterID = okLoot and itemInfo and tonumber(itemInfo.encounterID) or nil
        local itemID = okLoot and itemInfo and tonumber(itemInfo.itemID) or nil
        if encounterID and itemID then
            local encounterName
            local encounterDescription
            local encounterIcon
            if type(EJ_GetEncounterInfo) == "function" then
                local okEncounter, name, description = pcall(EJ_GetEncounterInfo, encounterID)
                if okEncounter then
                    encounterName = name
                    encounterDescription = description
                end
            end
            if type(EJ_GetCreatureInfo) == "function" then
                local okCreature, _, _, _, _, iconImage = pcall(EJ_GetCreatureInfo, 1, encounterID)
                if okCreature and iconImage then
                    encounterIcon = iconImage
                end
            end
            local bucket = encounters[encounterID]
            if not bucket then
                bucket = {
                    encounterID = encounterID,
                    name = (type(encounterName) == "string" and encounterName ~= "") and encounterName or ("Encounter " .. tostring(encounterID)),
                    description = (type(encounterDescription) == "string" and encounterDescription ~= "") and encounterDescription or "",
                    icon = encounterIcon,
                    items = {},
                }
                encounters[encounterID] = bucket
            end

            bucket.items[#bucket.items + 1] = {
                name = (type(itemInfo.name) == "string" and itemInfo.name ~= "") and itemInfo.name or ("Item " .. tostring(itemID)),
                slot = (type(itemInfo.slot) == "string" and itemInfo.slot ~= "") and itemInfo.slot or (L.UNKNOWN or "Unknown"),
                restriction = (type(itemInfo.armorType) == "string" and itemInfo.armorType ~= "") and itemInfo.armorType
                    or (L.RAID_JOURNAL_ALL_CLASSES or "All classes"),
                icon = itemInfo.icon,
                itemID = itemID,
                link = itemInfo.link,
                armorType = itemInfo.armorType,
                isVeryRare = itemInfo.displayAsVeryRare,
                isExtremelyRare = itemInfo.displayAsExtremelyRare,
                isPerPlayerLoot = itemInfo.displayAsPerPlayerLoot,
            }
        end
    end

    return encounters
end

local function buildEncounterJournalMidnightInstances(character, mainTabKey, difficultyKey)
    local journalConfig = getJournalModeConfig(mainTabKey)
    if type(EJ_GetNumTiers) ~= "function" or type(EJ_GetTierInfo) ~= "function" or type(EJ_SelectTier) ~= "function"
        or type(EJ_GetInstanceByIndex) ~= "function"
    then
        return nil
    end

    local tierCount = EJ_GetNumTiers() or 0
    local midnightTierIndex
    for tierIndex = 1, tierCount do
        local ok, tierName = pcall(EJ_GetTierInfo, tierIndex)
        if ok and type(tierName) == "string" and normalizeRaidJournalKey(tierName):find("midnight", 1, true) then
            midnightTierIndex = tierIndex
            break
        end
    end

    if not midnightTierIndex then
        return nil
    end

    pcall(EJ_SelectTier, midnightTierIndex)

    local isRepeatableDungeonDifficulty = (not journalConfig.isRaid) and (not usesDungeonDailyCounter(difficultyKey))
    local lockouts = isRepeatableDungeonDifficulty
        and {}
        or buildCurrentEncounterLockoutLookup(
            character,
            journalConfig.isRaid,
            (not journalConfig.isRaid) and journalConfig.difficultyIDs.mythic or nil
        )
    local instances = {}
    local instanceIndex = 1
    while true do
        local ok, instanceID, name, description = pcall(EJ_GetInstanceByIndex, instanceIndex, journalConfig.isRaid)
        if not ok or not instanceID then
            break
        end

        if type(name) == "string" and name ~= "" then
            local instanceKey = normalizeRaidJournalKey(name)
            local instanceEntry = {
                key = instanceKey,
                instanceID = tonumber(instanceID),
                name = name,
                subtitle = L.RAID_JOURNAL_SOURCE_EJ or "Encounter Journal",
                description = type(description) == "string" and description or "",
                bosses = {},
                killedCount = 0,
                totalCount = 0,
                progressText = isRepeatableDungeonDifficulty and nil or "0/0",
                counterMode = journalConfig.isRaid and "weekly" or (isRepeatableDungeonDifficulty and "repeatable" or "daily"),
            }

            if type(EJ_SelectInstance) == "function" then
                pcall(EJ_SelectInstance, instanceID)
            end
            if type(EJ_SetDifficulty) == "function" then
                pcall(EJ_SetDifficulty, journalConfig.difficultyIDs.normal or RAID_DIFFICULTY_IDS.normal)
            end
            if type(EJ_GetInstanceInfo) == "function" then
                local okInfo, _, _, bgImage, buttonImage1, _, buttonImage2 = pcall(EJ_GetInstanceInfo, instanceID)
                if okInfo then
                    instanceEntry.icon = buttonImage1 or buttonImage2 or bgImage or nil
                end
            end
            local encounterIndex = 1
            while true do
                local okEncounter, encounterName, encounterDescription, encounterID = pcall(EJ_GetEncounterInfoByIndex, encounterIndex, instanceID)
                if not okEncounter or not encounterName then
                    break
                end

                local bossKey = normalizeRaidJournalKey(encounterName)
                local killed = lockouts[instanceKey] and lockouts[instanceKey].kills and lockouts[instanceKey].kills[bossKey] == true
                local encounterIcon
                if type(EJ_GetCreatureInfo) == "function" and tonumber(encounterID) then
                    local okCreature, _, _, _, _, iconImage = pcall(EJ_GetCreatureInfo, 1, encounterID)
                    if okCreature and iconImage then
                        encounterIcon = iconImage
                    end
                end
                instanceEntry.totalCount = instanceEntry.totalCount + 1
                if killed then
                    instanceEntry.killedCount = instanceEntry.killedCount + 1
                end

                instanceEntry.bosses[#instanceEntry.bosses + 1] = {
                    key = bossKey,
                    name = encounterName,
                    description = type(encounterDescription) == "string" and encounterDescription or "",
                    encounterID = tonumber(encounterID),
                    icon = encounterIcon,
                    status = isRepeatableDungeonDifficulty and "repeatable" or (killed and "complete" or "open"),
                }
                encounterIndex = encounterIndex + 1
            end

            if #instanceEntry.bosses > 0 then
                instanceEntry.icon = instanceEntry.icon or (instanceEntry.bosses[1] and instanceEntry.bosses[1].icon) or nil
                if not isRepeatableDungeonDifficulty then
                    instanceEntry.progressText = string.format("%d/%d", instanceEntry.killedCount, instanceEntry.totalCount)
                end
                instances[#instances + 1] = instanceEntry
            end
        end

        instanceIndex = instanceIndex + 1
    end

    return #instances > 0 and instances or nil
end

local function buildEncounterJournalMidnightRaids(character)
    return buildEncounterJournalMidnightInstances(character, "raids", "normal")
end

local function buildEncounterJournalMidnightDungeons(character, difficultyKey)
    return buildEncounterJournalMidnightInstances(character, "dungeons", difficultyKey)
end

local function buildFallbackMidnightRaids()
    local raids = copyRaidJournalTable(MIDNIGHT_RAID_FALLBACK)
    for _, raid in ipairs(raids) do
        raid.totalCount = #raid.bosses
        raid.killedCount = 0
        raid.progressText = string.format("%d/%d", raid.killedCount, raid.totalCount)
    end
    return raids
end

local function buildFallbackMidnightDungeons()
    local dungeons = copyRaidJournalTable(MIDNIGHT_DUNGEON_FALLBACK)
    for _, dungeon in ipairs(dungeons) do
        dungeon.totalCount = #dungeon.bosses
        dungeon.killedCount = 0
        dungeon.progressText = string.format("%d/%d", dungeon.killedCount, dungeon.totalCount)
    end
    return dungeons
end

local function buildFallbackDungeonJournalEntries(subTabKey)
    local journalConfig = getJournalModeConfig("dungeons", subTabKey)
    if subTabKey == "midnight" then
        return buildFallbackMidnightDungeons()
    end

    return {
        {
            key = normalizeRaidJournalKey(journalConfig.subTabLabel),
            name = journalConfig.subTabLabel,
            subtitle = L.RAID_JOURNAL_SOURCE_EJ or "Encounter Journal",
            description = journalConfig.fallbackDescription,
            bosses = {
                {
                    key = "journal-sync",
                    name = L.RAID_JOURNAL_SYNC_TITLE or "Journal Sync",
                    description = L.RAID_JOURNAL_SYNC_DESCRIPTION or "The framework is in place. Live boss names, weekly kill tracking, and full loot sync are the next layer.",
                    status = "missing",
                    loot = {
                        {
                            name = L.RAID_JOURNAL_LOOT_PENDING or "Loot hookup pending",
                            slot = L.RAID_JOURNAL_LOOT_PENDING_SLOT or "Next step",
                            restriction = L.RAID_JOURNAL_LOOT_PENDING_RESTRICTION or "All classes",
                        },
                    },
                },
            },
            totalCount = 1,
            killedCount = 0,
            progressText = "0/1",
        },
    }
end

local function buildEncounterJournalInstanceEntry(character, journalConfig, difficultyKey, instanceID, name, description, tierIndex, lockouts)
    if type(instanceID) ~= "number" or instanceID <= 0 or type(name) ~= "string" or name == "" then
        return nil
    end

    local isRepeatableDungeonDifficulty = (not journalConfig.isRaid) and (not usesDungeonDailyCounter(difficultyKey))
    local instanceKey = normalizeRaidJournalKey(name)
    local instanceEntry = {
        key = instanceKey,
        instanceID = tonumber(instanceID),
        tierIndex = tonumber(tierIndex),
        name = name,
        subtitle = L.RAID_JOURNAL_SOURCE_EJ or "Encounter Journal",
        description = type(description) == "string" and description or "",
        bosses = {},
        killedCount = 0,
        totalCount = 0,
        progressText = isRepeatableDungeonDifficulty and nil or "0/0",
        counterMode = journalConfig.isRaid and "weekly" or (isRepeatableDungeonDifficulty and "repeatable" or "daily"),
    }

    if type(EJ_SelectTier) == "function" and type(tierIndex) == "number" and tierIndex > 0 then
        pcall(EJ_SelectTier, tierIndex)
    end
    if type(EJ_SelectInstance) == "function" then
        pcall(EJ_SelectInstance, instanceID)
    end
    if type(EJ_SetDifficulty) == "function" then
        pcall(EJ_SetDifficulty, journalConfig.difficultyIDs.normal or RAID_DIFFICULTY_IDS.normal)
    end
    if type(EJ_GetInstanceInfo) == "function" then
        local okInfo, _, _, bgImage, buttonImage1, _, buttonImage2 = pcall(EJ_GetInstanceInfo, instanceID)
        if okInfo then
            instanceEntry.icon = buttonImage1 or buttonImage2 or bgImage or nil
        end
    end
    local encounterIndex = 1
    while true do
        local okEncounter, encounterName, encounterDescription, encounterID = pcall(EJ_GetEncounterInfoByIndex, encounterIndex, instanceID)
        if not okEncounter or not encounterName then
            break
        end

        local bossKey = normalizeRaidJournalKey(encounterName)
        local killed = lockouts[instanceKey] and lockouts[instanceKey].kills and lockouts[instanceKey].kills[bossKey] == true
        local encounterIcon
        if type(EJ_GetCreatureInfo) == "function" and tonumber(encounterID) then
            local okCreature, _, _, _, _, iconImage = pcall(EJ_GetCreatureInfo, 1, encounterID)
            if okCreature and iconImage then
                encounterIcon = iconImage
            end
        end
        instanceEntry.totalCount = instanceEntry.totalCount + 1
        if killed then
            instanceEntry.killedCount = instanceEntry.killedCount + 1
        end

        instanceEntry.bosses[#instanceEntry.bosses + 1] = {
            key = bossKey,
            name = encounterName,
            description = type(encounterDescription) == "string" and encounterDescription or "",
            encounterID = tonumber(encounterID),
            icon = encounterIcon,
            status = isRepeatableDungeonDifficulty and "repeatable" or (killed and "complete" or "open"),
        }
        encounterIndex = encounterIndex + 1
    end

    if #instanceEntry.bosses == 0 then
        local fallbackDifficultyID = (journalConfig.difficultyIDs and journalConfig.difficultyIDs[difficultyKey])
            or journalConfig.difficultyIDs.normal
            or RAID_DIFFICULTY_IDS.normal
        local lootEncounterMap = buildEncounterJournalLootEncounterMap(instanceID, tierIndex, fallbackDifficultyID)
        local orderedEncounters = {}
        for encounterID, data in pairs(lootEncounterMap) do
            orderedEncounters[#orderedEncounters + 1] = data
        end
        table.sort(orderedEncounters, function(a, b)
            return (tonumber(a and a.encounterID) or 0) < (tonumber(b and b.encounterID) or 0)
        end)

        for _, data in ipairs(orderedEncounters) do
            local bossKey = normalizeRaidJournalKey(data.name)
            local killed = lockouts[instanceKey] and lockouts[instanceKey].kills and lockouts[instanceKey].kills[bossKey] == true
            instanceEntry.totalCount = instanceEntry.totalCount + 1
            if killed then
                instanceEntry.killedCount = instanceEntry.killedCount + 1
            end
            instanceEntry.bosses[#instanceEntry.bosses + 1] = {
                key = bossKey,
                name = data.name,
                description = type(data.description) == "string" and data.description or "",
                encounterID = data.encounterID,
                icon = data.icon,
                status = isRepeatableDungeonDifficulty and "repeatable" or (killed and "complete" or "open"),
                loot = data.items,
            }
        end
    end

    if #instanceEntry.bosses == 0 then
        return nil
    end

    instanceEntry.icon = instanceEntry.icon or (instanceEntry.bosses[1] and instanceEntry.bosses[1].icon) or nil
    if not isRepeatableDungeonDifficulty then
        instanceEntry.progressText = string.format("%d/%d", instanceEntry.killedCount, instanceEntry.totalCount)
    end

    return instanceEntry
end

local function buildEncounterJournalDungeonCatalog()
    if type(EJ_GetNumTiers) ~= "function" or type(EJ_SelectTier) ~= "function" or type(EJ_GetInstanceByIndex) ~= "function" then
        return {}
    end

    local catalog = {}
    local tierCount = EJ_GetNumTiers() or 0
    local savedTier = type(EJ_GetCurrentTier) == "function" and EJ_GetCurrentTier() or nil
    for tierIndex = 1, tierCount do
        pcall(EJ_SelectTier, tierIndex)
        local instanceIndex = 1
        while true do
            local ok, instanceID, name, description = pcall(EJ_GetInstanceByIndex, instanceIndex, false)
            if not ok or not instanceID then
                break
            end

            if type(name) == "string" and name ~= "" then
                local instanceKey = normalizeRaidJournalKey(name)
                catalog[instanceKey] = catalog[instanceKey] or {}
                catalog[instanceKey][#catalog[instanceKey] + 1] = {
                    instanceID = tonumber(instanceID),
                    tierIndex = tonumber(tierIndex),
                    name = name,
                    description = type(description) == "string" and description or "",
                }
            end

            instanceIndex = instanceIndex + 1
        end
    end

    if savedTier then
        pcall(EJ_SelectTier, savedTier)
    end

    return catalog
end

local SEASONAL_DUNGEON_CANONICAL_KEYS = {}
for canonicalKey, keyGroup in pairs(SEASONAL_DUNGEON_KEY_GROUPS) do
    for _, aliasKey in ipairs(keyGroup) do
        SEASONAL_DUNGEON_CANONICAL_KEYS[aliasKey] = canonicalKey
    end
end

local OFFICIAL_SEASON_ONE_LEGACY_DUNGEON_LOOKUP = {}
for _, dungeonKey in ipairs(OFFICIAL_SEASON_ONE_LEGACY_DUNGEON_KEYS) do
    OFFICIAL_SEASON_ONE_LEGACY_DUNGEON_LOOKUP[dungeonKey] = true
end

local function getCanonicalSeasonalDungeonKey(dungeonKey)
    if type(dungeonKey) ~= "string" or dungeonKey == "" then
        return ""
    end

    return SEASONAL_DUNGEON_CANONICAL_KEYS[dungeonKey] or dungeonKey
end

local function getSeasonalDungeonCatalogCandidates(dungeonCatalog, dungeonKey)
    local canonicalKey = getCanonicalSeasonalDungeonKey(dungeonKey)
    local candidateKeys = SEASONAL_DUNGEON_KEY_GROUPS[canonicalKey] or { canonicalKey }
    local candidates = {}
    local seenInstanceIDs = {}

    for _, candidateKey in ipairs(candidateKeys) do
        for _, entry in ipairs(dungeonCatalog[candidateKey] or {}) do
            local instanceID = tonumber(entry and entry.instanceID)
            if instanceID and not seenInstanceIDs[instanceID] then
                seenInstanceIDs[instanceID] = true
                candidates[#candidates + 1] = entry
            end
        end
    end

    if #candidates == 0 and canonicalKey == "seat-of-the-triumvirate" then
        for catalogKey, entries in pairs(dungeonCatalog) do
            if type(catalogKey) == "string" and catalogKey:find("triumvirate", 1, true) then
                for _, entry in ipairs(entries or {}) do
                    local instanceID = tonumber(entry and entry.instanceID)
                    if instanceID and not seenInstanceIDs[instanceID] then
                        seenInstanceIDs[instanceID] = true
                        candidates[#candidates + 1] = entry
                    end
                end
            end
        end
    end

    return candidates
end

local function buildEncounterJournalInstanceLookup(dungeonCatalog)
    local lookup = {}
    for _, entries in pairs(dungeonCatalog or {}) do
        for _, entry in ipairs(entries or {}) do
            local instanceID = tonumber(entry and entry.instanceID)
            if instanceID and not lookup[instanceID] then
                lookup[instanceID] = entry
            end
        end
    end
    return lookup
end

local function resolveSeasonalDungeonJournalEntry(mapChallengeModeID, dungeonCatalog, instanceLookup)
    local okMap, dungeonName, _, _, _, uiMapID = pcall(C_ChallengeMode.GetMapUIInfo, mapChallengeModeID)
    if not okMap or type(dungeonName) ~= "string" or dungeonName == "" then
        return nil
    end

    local ejEntry
    if uiMapID and type(EJ_GetInstanceForMap) == "function" then
        local okInstance, instanceID = pcall(EJ_GetInstanceForMap, uiMapID)
        if okInstance and tonumber(instanceID) and tonumber(instanceID) > 0 then
            ejEntry = instanceLookup[tonumber(instanceID)]
        end
    end

    if not ejEntry then
        local candidates = getSeasonalDungeonCatalogCandidates(dungeonCatalog, normalizeRaidJournalKey(dungeonName))
        ejEntry = candidates[1]
    end

    if not ejEntry then
        return nil
    end

    local resolvedName = (type(ejEntry.name) == "string" and ejEntry.name ~= "") and ejEntry.name or dungeonName
    return {
        key = normalizeRaidJournalKey(resolvedName),
        canonicalKey = getCanonicalSeasonalDungeonKey(normalizeRaidJournalKey(dungeonName)),
        challengeModeID = tonumber(mapChallengeModeID),
        uiMapID = tonumber(uiMapID),
        instanceID = tonumber(ejEntry.instanceID),
        tierIndex = tonumber(ejEntry.tierIndex),
        name = resolvedName,
        description = type(ejEntry.description) == "string" and ejEntry.description or "",
        rawName = dungeonName,
    }
end

local function tryAddSeasonalDungeonByKey(dungeons, seenKeys, seenInstanceIDs, dungeonCatalog, dungeonKey, character, journalConfig, difficultyKey, lockouts)
    local canonicalKey = getCanonicalSeasonalDungeonKey(dungeonKey)
    if canonicalKey == "" or seenKeys[canonicalKey] then
        return false
    end

    local candidates = getSeasonalDungeonCatalogCandidates(dungeonCatalog, canonicalKey)
    for _, candidate in ipairs(candidates) do
        local dungeonEntry = buildEncounterJournalInstanceEntry(
            character,
            journalConfig,
            difficultyKey,
            candidate.instanceID,
            candidate.name,
            candidate.description,
            candidate.tierIndex,
            lockouts
        )
        if dungeonEntry and not seenInstanceIDs[tonumber(dungeonEntry.instanceID) or 0] then
            dungeons[#dungeons + 1] = dungeonEntry
            seenKeys[canonicalKey] = true
            seenInstanceIDs[tonumber(dungeonEntry.instanceID) or 0] = true
            return true
        end
    end

    return false
end

local function requestCurrentSeasonMapInfo()
    if C_MythicPlus and type(C_MythicPlus.RequestMapInfo) == "function" then
        pcall(C_MythicPlus.RequestMapInfo)
    end

    if C_ChallengeMode and type(C_ChallengeMode.RequestMapInfo) == "function" then
        pcall(C_ChallengeMode.RequestMapInfo)
    end
end

local function buildEncounterJournalSeasonOneDungeons(character, difficultyKey)
    local journalConfig = getJournalModeConfig("dungeons", "season1")
    if type(C_ChallengeMode) ~= "table"
        or type(C_ChallengeMode.GetMapTable) ~= "function"
        or type(C_ChallengeMode.GetMapUIInfo) ~= "function"
    then
        return nil
    end

    requestCurrentSeasonMapInfo()

    local seasonalMapIDs = C_ChallengeMode.GetMapTable()
    if type(seasonalMapIDs) ~= "table" or #seasonalMapIDs == 0 then
        return nil
    end

    local midnightDungeons = buildEncounterJournalMidnightDungeons(character, difficultyKey) or {}
    local midnightKeys = {}
    local midnightInstanceIDs = {}
    for _, dungeon in ipairs(midnightDungeons) do
        midnightKeys[getCanonicalSeasonalDungeonKey(dungeon.key)] = true
        if tonumber(dungeon.instanceID) and tonumber(dungeon.instanceID) > 0 then
            midnightInstanceIDs[tonumber(dungeon.instanceID)] = true
        end
    end

    local isRepeatableDungeonDifficulty = not usesDungeonDailyCounter(difficultyKey)
    local lockouts = isRepeatableDungeonDifficulty
        and {}
        or buildCurrentEncounterLockoutLookup(character, false, journalConfig.difficultyIDs.mythic)
    local dungeonCatalog = buildEncounterJournalDungeonCatalog()
    local instanceLookup = buildEncounterJournalInstanceLookup(dungeonCatalog)
    local dungeons = {}
    local seenKeys = {}
    local seenInstanceIDs = {}

    for _, mapChallengeModeID in ipairs(seasonalMapIDs) do
        local resolvedEntry = resolveSeasonalDungeonJournalEntry(mapChallengeModeID, dungeonCatalog, instanceLookup)
        if resolvedEntry and resolvedEntry.instanceID then
            local canonicalKey = getCanonicalSeasonalDungeonKey(resolvedEntry.key)
            if canonicalKey ~= "" and not midnightKeys[canonicalKey] and not midnightInstanceIDs[resolvedEntry.instanceID] and not seenInstanceIDs[resolvedEntry.instanceID] then
                local dungeonEntry = buildEncounterJournalInstanceEntry(
                    character,
                    journalConfig,
                    difficultyKey,
                    resolvedEntry.instanceID,
                    resolvedEntry.name,
                    resolvedEntry.description,
                    resolvedEntry.tierIndex,
                    lockouts
                )
                if dungeonEntry then
                    dungeons[#dungeons + 1] = dungeonEntry
                    seenKeys[canonicalKey] = true
                    seenInstanceIDs[resolvedEntry.instanceID] = true
                end
            end
        end
    end

    for _, dungeonKey in ipairs(OFFICIAL_SEASON_ONE_LEGACY_DUNGEON_KEYS) do
        if not seenKeys[dungeonKey] then
            tryAddSeasonalDungeonByKey(dungeons, seenKeys, seenInstanceIDs, dungeonCatalog, dungeonKey, character, journalConfig, difficultyKey, lockouts)
        end
    end

    return #dungeons > 0 and dungeons or nil
end

local function normalizeJournalDifficultyKey(difficultyKey, mainTabKey)
    for _, option in ipairs(getJournalDifficultyOptions(mainTabKey)) do
        if option.key == difficultyKey then
            return option.key
        end
    end

    local options = getJournalDifficultyOptions(mainTabKey)
    return options[1] and options[1].key or "normal"
end

local function buildJournalView(character, mainTabKey, subTabKey)
    local journalConfig = getJournalModeConfig(mainTabKey, subTabKey)
    local state = ns:GetJournalState(mainTabKey)
    local difficultyKey = normalizeJournalDifficultyKey(state.difficultyKey, mainTabKey)
    local classFilterKey = state.classFilterKey == "all" and "all" or "player"
    local classFilter = getRaidClassFilterSelection(character, classFilterKey)
    local trackerCharacterKey = character and character.key or (ns.GetCurrentCharacterInfo and ns:GetCurrentCharacterInfo().key) or nil
    local raids
    if journalConfig.isRaid then
        raids = buildEncounterJournalMidnightRaids(character) or buildFallbackMidnightRaids()
    elseif subTabKey == "season1" then
        raids = buildEncounterJournalSeasonOneDungeons(character, difficultyKey) or buildFallbackDungeonJournalEntries("season1")
    else
        raids = buildEncounterJournalMidnightDungeons(character, difficultyKey) or buildFallbackDungeonJournalEntries("midnight")
    end
    raids = type(raids) == "table" and raids or {}
    local selectedRaid = raids[1]
    local bossLootCache = {}

    if mainTabKey == "dungeons" and not usesDungeonDailyCounter(difficultyKey) then
        for _, dungeon in ipairs(raids) do
            dungeon.counterMode = "repeatable"
            dungeon.killedCount = 0
            dungeon.progressText = nil
            for _, boss in ipairs(dungeon.bosses or {}) do
                boss.status = "repeatable"
            end
        end
    end

    for _, raid in ipairs(raids) do
        if raid.key == state.selectedRaidKey then
            selectedRaid = raid
            break
        end
    end

    local selectedBoss = selectedRaid and selectedRaid.bosses and selectedRaid.bosses[1] or nil
    if selectedRaid then
        for _, boss in ipairs(selectedRaid.bosses or {}) do
            if boss.key == state.selectedBossKey then
                selectedBoss = boss
                break
            end
        end
    end

    local function getBossLoot(raid, boss)
        if not raid or not boss then
            return {}
        end

        bossLootCache[raid.key] = bossLootCache[raid.key] or {}
        if bossLootCache[raid.key][boss.key] then
            return bossLootCache[raid.key][boss.key]
        end

        local items = buildEncounterJournalLoot(
            raid.instanceID,
            boss.encounterID,
            difficultyKey,
            classFilter.key,
            character,
            mainTabKey,
            raid.tierIndex
        ) or {}

        if #items == 0 then
            items = copyRaidJournalTable(boss.loot or {})
        end

        bossLootCache[raid.key][boss.key] = items
        return items
    end

    for _, raid in ipairs(raids) do
        raid.icon = raid.icon or (raid.bosses and raid.bosses[1] and raid.bosses[1].icon) or nil
        raid.trackerSummary = nil
        for _, boss in ipairs(raid.bosses or {}) do
            local bossLoot = getBossLoot(raid, boss)
            boss.trackerSummary = buildRaidLootTrackerSummary(trackerCharacterKey, difficultyKey, bossLoot)
            raid.trackerSummary = mergeRaidLootTrackerSummary(raid.trackerSummary, boss.trackerSummary)
        end
    end

    local loot = selectedBoss and getBossLoot(selectedRaid, selectedBoss) or {}
    if #loot == 0 then
        loot = copyRaidJournalTable((selectedBoss and selectedBoss.loot) or {
            {
                name = L.RAID_JOURNAL_LOOT_PENDING or "Loot hookup pending",
                slot = L.RAID_JOURNAL_LOOT_PENDING_SLOT or "Next step",
                restriction = classFilter.key ~= "all"
                    and (classFilter.className or (L.UNKNOWN or "Unknown"))
                    or (L.RAID_JOURNAL_ALL_CLASSES or "All classes"),
            },
        })
    end

    return {
        raids = raids,
        selectedRaid = selectedRaid,
        selectedBoss = selectedBoss,
        selectedRaidKey = selectedRaid and selectedRaid.key or "",
        selectedBossKey = selectedBoss and selectedBoss.key or "",
        difficultyKey = difficultyKey,
        classFilterKey = classFilter.key,
        classFilterName = classFilter.displayName,
        classFilterMode = classFilter.isPlayer and "player" or (classFilter.isAll and "all" or "custom"),
        difficultyOptions = journalConfig.difficultyOptions,
        primaryLabel = journalConfig.primaryLabel,
        primaryLabelPlural = journalConfig.primaryLabelPlural,
        bossesLabel = journalConfig.bossesLabel,
        listSubtitle = journalConfig.listSubtitle,
        subTabLabel = journalConfig.subTabLabel,
        mainTabKey = journalConfig.mainTabKey,
        loot = loot,
    }
end

local function buildMidnightRaidJournalView(character)
    return buildJournalView(character, "raids", "midnight")
end

local function buildMidnightDungeonJournalView(character)
    return buildJournalView(character, "dungeons", "midnight")
end

local function buildSeasonOneDungeonJournalView(character)
    return buildJournalView(character, "dungeons", "season1")
end

local function getWishlistDifficultySortValue(difficultyKey)
    if difficultyKey == "lfr" then
        return 1
    end
    if difficultyKey == "normal" then
        return 2
    end
    if difficultyKey == "heroic" then
        return 3
    end
    if difficultyKey == "mythic" then
        return 4
    end

    return 99
end

local function getWishlistFallbackItemName(itemID)
    if C_Item and C_Item.GetItemNameByID then
        local itemName = C_Item.GetItemNameByID(itemID)
        if type(itemName) == "string" and itemName ~= "" then
            return itemName
        end
    end

    if type(GetItemInfo) == "function" then
        local itemName = GetItemInfo(itemID)
        if type(itemName) == "string" and itemName ~= "" then
            return itemName
        end
    end

    if C_Item and C_Item.RequestLoadItemDataByID then
        pcall(C_Item.RequestLoadItemDataByID, itemID)
    end

    return string.format("Item %s", tostring(itemID or "?"))
end

local function getWishlistFallbackItemIcon(itemID)
    if C_Item and C_Item.GetItemIconByID then
        local itemIcon = C_Item.GetItemIconByID(itemID)
        if itemIcon then
            return itemIcon
        end
    end

    return "Interface\\Icons\\INV_Misc_QuestionMark"
end

local function buildWishlistSourceIndex(character)
    local sourceConfigs = {
        {
            mainTabKey = "raids",
            subTabKey = "midnight",
            difficultyOptions = RAID_DIFFICULTY_OPTIONS,
            buildInstances = function(selectedCharacter)
                return buildEncounterJournalMidnightRaids(selectedCharacter) or buildFallbackMidnightRaids()
            end,
        },
        {
            mainTabKey = "dungeons",
            subTabKey = "midnight",
            difficultyOptions = DUNGEON_DIFFICULTY_OPTIONS,
            buildInstances = function(selectedCharacter, difficultyKey)
                return buildEncounterJournalMidnightDungeons(selectedCharacter, difficultyKey) or buildFallbackDungeonJournalEntries("midnight")
            end,
        },
        {
            mainTabKey = "dungeons",
            subTabKey = "season1",
            difficultyOptions = DUNGEON_DIFFICULTY_OPTIONS,
            buildInstances = function(selectedCharacter, difficultyKey)
                return buildEncounterJournalSeasonOneDungeons(selectedCharacter, difficultyKey) or buildFallbackDungeonJournalEntries("season1")
            end,
        },
    }

    local sourceIndex = {}

    local function registerLootEntry(sourceConfig, difficultyKey, raid, boss, loot)
        local itemID = loot and tonumber(loot.itemID) or nil
        if not itemID or itemID <= 0 then
            return
        end

        local entryKey = string.format("%s:%d", difficultyKey or "normal", itemID)
        local bucket = sourceIndex[entryKey]
        if not bucket then
            bucket = {
                itemID = itemID,
                difficultyKey = difficultyKey,
                loot = copyRaidJournalTable(loot),
                sources = {},
                sourceLookup = {},
            }
            sourceIndex[entryKey] = bucket
        else
            if (not bucket.loot.link) and loot.link then
                bucket.loot.link = loot.link
            end
            if (not bucket.loot.icon) and loot.icon then
                bucket.loot.icon = loot.icon
            end
            if (not bucket.loot.name or bucket.loot.name == "") and loot.name then
                bucket.loot.name = loot.name
            end
        end

        local sourceKey = string.format(
            "%s:%s:%s:%s:%s",
            tostring(sourceConfig.mainTabKey or ""),
            tostring(sourceConfig.subTabKey or ""),
            tostring(raid and (raid.key or raid.instanceID) or ""),
            tostring(boss and (boss.key or boss.encounterID) or ""),
            tostring(difficultyKey or "")
        )
        if bucket.sourceLookup[sourceKey] then
            return
        end

        bucket.sourceLookup[sourceKey] = true
        bucket.sources[#bucket.sources + 1] = {
            mainTabKey = sourceConfig.mainTabKey,
            subTabKey = sourceConfig.subTabKey,
            difficultyKey = difficultyKey,
            raidKey = raid and raid.key or nil,
            bossKey = boss and boss.key or nil,
            instanceID = raid and raid.instanceID or nil,
            encounterID = boss and boss.encounterID or nil,
            instanceName = raid and raid.name or nil,
            bossName = boss and boss.name or nil,
        }
    end

    for _, sourceConfig in ipairs(sourceConfigs) do
        local raidInstancesCache = nil
        for _, difficultyOption in ipairs(sourceConfig.difficultyOptions or {}) do
            local difficultyKey = difficultyOption.key
            local instances
            if sourceConfig.mainTabKey == "raids" then
                raidInstancesCache = raidInstancesCache or sourceConfig.buildInstances(character, difficultyKey) or {}
                instances = raidInstancesCache
            else
                instances = sourceConfig.buildInstances(character, difficultyKey) or {}
            end

            for _, raid in ipairs(instances) do
                for _, boss in ipairs(raid.bosses or {}) do
                    local lootEntries = buildEncounterJournalLoot(
                        raid.instanceID,
                        boss.encounterID,
                        difficultyKey,
                        "all",
                        character,
                        sourceConfig.mainTabKey,
                        raid.tierIndex
                    ) or {}

                    if #lootEntries == 0 then
                        lootEntries = copyRaidJournalTable(boss.loot or {})
                    end

                    for _, loot in ipairs(lootEntries) do
                        registerLootEntry(sourceConfig, difficultyKey, raid, boss, loot)
                    end
                end
            end
        end
    end

    for _, bucket in pairs(sourceIndex) do
        bucket.sourceLookup = nil
    end

    return sourceIndex
end

local function buildWishlistEntriesForCharacter(character, filterKey)
    local characterKey = character and character.key or nil
    local trackedEntries = (characterKey and ns.GetRaidLootTrackerEntries and ns:GetRaidLootTrackerEntries(characterKey, "wish")) or {}
    if #trackedEntries == 0 then
        return {}, { total = 0, raids = 0, dungeons = 0 }
    end

    local sourceIndex = buildWishlistSourceIndex(character)
    local entries = {}
    local counts = {
        total = 0,
        raids = 0,
        dungeons = 0,
    }

    for _, trackedEntry in ipairs(trackedEntries) do
        local entryKey = string.format("%s:%d", trackedEntry.difficultyKey or "normal", trackedEntry.itemID)
        local sourceData = sourceIndex[entryKey]
        local entry = {
            key = entryKey,
            itemID = trackedEntry.itemID,
            difficultyKey = trackedEntry.difficultyKey,
            state = trackedEntry.state,
            loot = sourceData and copyRaidJournalTable(sourceData.loot) or {
                itemID = trackedEntry.itemID,
                name = getWishlistFallbackItemName(trackedEntry.itemID),
                icon = getWishlistFallbackItemIcon(trackedEntry.itemID),
            },
            sources = sourceData and copyRaidJournalTable(sourceData.sources) or {},
        }

        entry.loot.itemID = entry.loot.itemID or trackedEntry.itemID
        entry.loot.name = (type(entry.loot.name) == "string" and entry.loot.name ~= "")
            and entry.loot.name
            or getWishlistFallbackItemName(trackedEntry.itemID)
        entry.loot.icon = entry.loot.icon or getWishlistFallbackItemIcon(trackedEntry.itemID)

        local primarySource = entry.sources[1]
        local matchesFilter = filterKey ~= "raids" and filterKey ~= "dungeons"
            or (primarySource and primarySource.mainTabKey == filterKey)
        if matchesFilter then
            counts.total = counts.total + 1
            if primarySource and primarySource.mainTabKey == "raids" then
                counts.raids = counts.raids + 1
            elseif primarySource and primarySource.mainTabKey == "dungeons" then
                counts.dungeons = counts.dungeons + 1
            end
            entries[#entries + 1] = entry
        end
    end

    table.sort(entries, function(a, b)
        local aSource = a.sources and a.sources[1] or nil
        local bSource = b.sources and b.sources[1] or nil
        local aOrder = WISHLIST_CONTENT_SORT_ORDER[aSource and aSource.mainTabKey or ""] or 99
        local bOrder = WISHLIST_CONTENT_SORT_ORDER[bSource and bSource.mainTabKey or ""] or 99
        if aOrder ~= bOrder then
            return aOrder < bOrder
        end

        local aInstance = normalizeRaidJournalKey(aSource and aSource.instanceName or "")
        local bInstance = normalizeRaidJournalKey(bSource and bSource.instanceName or "")
        if aInstance ~= bInstance then
            return aInstance < bInstance
        end

        local aBoss = normalizeRaidJournalKey(aSource and aSource.bossName or "")
        local bBoss = normalizeRaidJournalKey(bSource and bSource.bossName or "")
        if aBoss ~= bBoss then
            return aBoss < bBoss
        end

        local aDifficulty = getWishlistDifficultySortValue(a.difficultyKey)
        local bDifficulty = getWishlistDifficultySortValue(b.difficultyKey)
        if aDifficulty ~= bDifficulty then
            return aDifficulty < bDifficulty
        end

        local aName = normalizeRaidJournalKey(a.loot and a.loot.name or "")
        local bName = normalizeRaidJournalKey(b.loot and b.loot.name or "")
        if aName ~= bName then
            return aName < bName
        end

        return (tonumber(a.itemID) or 0) < (tonumber(b.itemID) or 0)
    end)

    return entries, counts
end

JournalView.GetJournalDifficultyOptions = getJournalDifficultyOptions
JournalView.UsesDungeonDailyCounter = usesDungeonDailyCounter
JournalView.IsJournalSubTab = isJournalSubTab
JournalView.GetRaidDifficultyLabel = getRaidDifficultyLabel
JournalView.BuildRaidLootMetaLabel = buildRaidLootMetaLabel
JournalView.GetRaidLootTrackerVisual = getRaidLootTrackerVisual
JournalView.BuildRaidLootTrackerSummary = buildRaidLootTrackerSummary
JournalView.GetRaidLootTrackerBadgeVisual = getRaidLootTrackerBadgeVisual
JournalView.AddRaidLootTrackerSummaryTooltip = addRaidLootTrackerSummaryTooltip
JournalView.BuildWishlistEntryMeta = buildWishlistEntryMeta
JournalView.AddWishlistSourcesTooltip = addWishlistSourcesTooltip
JournalView.GetRaidLootQualityColor = getRaidLootQualityColor
JournalView.BuildMidnightRaidJournalView = buildMidnightRaidJournalView
JournalView.BuildMidnightDungeonJournalView = buildMidnightDungeonJournalView
JournalView.BuildSeasonOneDungeonJournalView = buildSeasonOneDungeonJournalView
JournalView.BuildWishlistEntriesForCharacter = buildWishlistEntriesForCharacter
