local addonName, ZL = ...

local CATEGORY_ORDER = {
    { key = "achievements", label = "Achievements" },
    { key = "pets", label = "Battle Pets" },
    { key = "decor", label = "Decor" },
    { key = "instances", label = "Instances" },
    { key = "mounts", label = "Mounts" },
    { key = "quests", label = "Quests" },
    { key = "rares", label = "Rare Drops" },
    { key = "transmogs", label = "Transmog" },
}

local function URLEncode(value)
    local plain = tostring(value or ""):gsub("|c%x%x%x%x%x%x%x%x", ""):gsub("|r", ""):gsub("|T.-|t", "")
    return plain:gsub("([^%w%-_%.~])", function(character)
        return string.format("%%%02X", string.byte(character))
    end)
end

local function Contains(values, wanted)
    if not values then return true end
    for _, value in ipairs(values) do if value == wanted then return true end end
    return false
end

local function IsEligible(entry, zoneDef)
    if type(entry) ~= "table" then entry = {} end
    local faction = entry.faction or zoneDef.faction
    local playerFaction = UnitFactionGroup("player")
    if faction == 1 and playerFaction ~= "Horde" then return false end
    if faction == 2 and playerFaction ~= "Alliance" then return false end
    local _, _, classID = UnitClass("player")
    local _, _, raceID = UnitRace("player")
    return Contains(entry.classes, classID) and Contains(entry.races, raceID)
end

local function EntryID(entry)
    return type(entry) == "table" and entry.id or entry
end

local function AddObjective(category, id, name, icon, complete, link, metadata)
    local resolvedName = name or ("Unknown #" .. id)
    category.total = category.total + 1
    if complete then category.done = category.done + 1 end
    category.objectives[#category.objectives + 1] = {
        id = id, name = resolvedName, icon = icon,
        complete = complete and true or false, link = link,
        source = metadata and metadata.source,
        location = metadata and metadata.location,
        categoryKey = category.key,
        itemID = metadata and metadata.itemID,
        sourceID = metadata and metadata.sourceID,
        decorID = metadata and metadata.decorID,
        coordMapID = metadata and metadata.coordMapID,
        coordX = metadata and metadata.coordX,
        coordY = metadata and metadata.coordY,
        transmogSource = metadata and metadata.transmogSource,
        wowheadURL = (metadata and metadata.wowheadURL) or
            ("https://www.wowhead.com/search?q=" .. URLEncode(resolvedName)),
    }
end

local function ScanAchievement(category, achievementID)
    local _, name, _, completed, _, _, _, _, _, icon, _, isGuildAchievement = GetAchievementInfo(achievementID)
    if isGuildAchievement then return end
    AddObjective(category, achievementID, name, icon, completed, GetAchievementLink(achievementID), {
        wowheadURL = "https://www.wowhead.com/achievement=" .. achievementID,
    })
end

local function ScanQuest(category, entry, zoneName)
    local questID = EntryID(entry)
    local name = C_QuestLog.GetTitleForQuestID(questID)
    if not name and C_QuestLog.RequestLoadQuestByID then C_QuestLog.RequestLoadQuestByID(questID) end
    local complete = C_QuestLog.IsQuestFlaggedCompleted(questID)
    for _, alternativeID in ipairs(type(entry) == "table" and entry.alternatives or {}) do
        complete = complete or C_QuestLog.IsQuestFlaggedCompleted(alternativeID)
    end
    AddObjective(category, questID, name or ("Quest " .. questID), 463446, complete, nil, {
        location = zoneName,
        wowheadURL = "https://www.wowhead.com/quest=" .. questID,
    })
end

local function ScanMount(category, entry)
    local mountID = EntryID(entry)
    if type(entry) == "table" and entry.itemID then
        mountID = C_MountJournal.GetMountFromItem(entry.itemID)
    elseif type(entry) == "table" and entry.spellID then
        for _, candidateID in ipairs(C_MountJournal.GetMountIDs()) do
            local _, spellID = C_MountJournal.GetMountInfoByID(candidateID)
            if spellID == entry.spellID then mountID = candidateID; break end
        end
    end
    if not mountID then return end
    local name, spellID, icon, _, _, _, _, _, _, _, collected = C_MountJournal.GetMountInfoByID(mountID)
    local itemID = type(entry) == "table" and entry.itemID
    AddObjective(category, mountID, name or ("Mount " .. mountID), icon or 132261, collected, nil, {
        wowheadURL = itemID and ("https://www.wowhead.com/item=" .. itemID) or
            (spellID and ("https://www.wowhead.com/spell=" .. spellID) or nil),
    })
end

local function ScanDecor(category, entry, zoneName)
    local decorID = EntryID(entry)
    if not decorID then return end
    local info
    if C_HousingCatalog and C_HousingCatalog.GetCatalogEntryInfoByRecordID and Enum and Enum.HousingCatalogEntryType then
        local ok, result = pcall(C_HousingCatalog.GetCatalogEntryInfoByRecordID,
            Enum.HousingCatalogEntryType.Decor, decorID, true)
        if ok then info = result end
    end
    local itemID = (info and info.itemID) or (type(entry) == "table" and entry.itemID)
    local name = info and (info.name or info.entryName)
    local icon = info and (info.icon or info.iconFileID)
    if itemID and not name then name = C_Item.GetItemNameByID and C_Item.GetItemNameByID(itemID) end
    local owned = info and ((info.numPlaced or 0) + (info.quantity or 0) + (info.remainingRedeemable or 0)) or 0
    AddObjective(category, decorID, name or ("Decor " .. decorID), icon or 134435, owned > 0, nil, {
        location = zoneName,
        itemID = itemID,
        decorID = decorID,
        wowheadURL = itemID and ("https://www.wowhead.com/item=" .. itemID) or nil,
    })
end

local function ScanPet(category, entry, zoneName)
    local speciesID = EntryID(entry)
    if not speciesID or not C_PetJournal then return end
    local name, icon = C_PetJournal.GetPetInfoBySpeciesID(speciesID)
    local collected = C_PetJournal.GetNumCollectedInfo(speciesID) or 0
    AddObjective(category, speciesID, name or ("Battle Pet " .. speciesID), icon or 656595, collected > 0, nil, {
        location = zoneName,
        wowheadURL = "https://www.wowhead.com/battle-pet=" .. speciesID,
    })
end

local function ScanTransmog(category, entry)
    local sourceID = EntryID(entry)
    local info = C_TransmogCollection.GetSourceInfo(sourceID)
    local appearanceInfo = C_TransmogCollection.GetAppearanceSourceInfo(sourceID)
    local itemID = info and info.itemID or (C_TransmogCollection.GetSourceItemID and C_TransmogCollection.GetSourceItemID(sourceID))
    local itemName, itemLink, _, _, _, _, _, _, _, icon = itemID and C_Item.GetItemInfo(itemID)
    if itemID and not itemName then C_Item.RequestLoadItemDataByID(itemID) end
    AddObjective(category, sourceID, itemName or ("Appearance " .. sourceID),
        icon or (appearanceInfo and appearanceInfo.icon) or 135048,
        (appearanceInfo and appearanceInfo.isCollected) or (info and info.isCollected),
        itemLink or (appearanceInfo and appearanceInfo.itemLink), {
            itemID = itemID,
            sourceID = sourceID,
            transmogSource = type(entry) == "table" and
                ((entry.isVendor or entry.vendorID or entry.sourceType == "vendor") and "vendor" or
                (entry.questID and "quest" or "zone")) or "zone",
            wowheadURL = itemID and ("https://www.wowhead.com/item=" .. itemID) or nil,
        })
end

local function ScanRare(category, entry)
    local itemID = entry.itemID
    local sourceInfo = entry.sourceID and C_TransmogCollection.GetSourceInfo(entry.sourceID)
    local appearanceInfo = entry.sourceID and C_TransmogCollection.GetAppearanceSourceInfo(entry.sourceID)
    local collected = (appearanceInfo and appearanceInfo.isCollected) or
        (sourceInfo and sourceInfo.isCollected) or (itemID and ZL_Saved.collectedItems[itemID])
    local itemName, itemLink, _, _, _, _, _, _, _, icon = itemID and C_Item.GetItemInfo(itemID)
    if itemID and not itemName then C_Item.RequestLoadItemDataByID(itemID) end
    AddObjective(category, itemID or entry.sourceID or entry.npcID,
        entry.name or itemName or ("Rare drop " .. (itemID or entry.sourceID or entry.npcID)),
        icon or 132176, collected, itemLink, {
            wowheadURL = itemID and ("https://www.wowhead.com/item=" .. itemID) or nil,
        })
end

function ZL:StoreZoneSummary(data)
    if not data or not data.mapID then return end
    local done, total = self:GetProgress(data)
    local complete = data.eligible and data.catalogComplete and (total == 0 or done == total)
    local categories, instances = {}, {}
    for _, category in ipairs(data.categories or {}) do
        local categoryDone, categoryTotal = self:GetCategoryProgress(category)
        categories[#categories + 1] = {
            key = category.key, name = category.name, done = categoryDone, total = categoryTotal,
            objectives = {}, instances = {}, summary = true,
        }
    end
    if complete then
        done = total
        for _, category in ipairs(categories) do category.done = category.total end
    end
    for _, instance in ipairs(data.instances or {}) do instances[#instances + 1] = { name = instance.name } end
    ZL_Saved.zoneSummaries[data.mapID] = {
        mapID = data.mapID, name = data.name, done = done, total = total,
        provider = data.provider, catalogComplete = data.catalogComplete,
        eligible = data.eligible, categories = categories, instances = instances,
        summary = true, complete = complete,
    }
    if complete then ZL_Saved.completedZones[data.mapID] = true end
end

function ZL:ScanZone(mapID)
    local definition = self:GetZoneDefinition(mapID)
    if self.ScanZoneFromATT then
        local attData, handledByATT = self:ScanZoneFromATT(mapID)
        if attData then
            self.ZoneData[mapID] = attData
            self:UpdateCompletionHistory(attData)
            self:StoreZoneSummary(attData)
            return attData
        elseif handledByATT then
            local existing = self.ZoneData[mapID]
            if existing then return existing end
            local loading = {
                mapID = mapID, name = definition.name, categories = {}, done = 0, total = 0,
                catalogComplete = false, eligible = true, provider = "ATT (loading)",
            }
            self.ZoneData[mapID] = loading
            return loading
        end
    end
    local zoneEligible = IsEligible({}, definition)
    local categories, byKey = {}, {}
    for _, descriptor in ipairs(CATEGORY_ORDER) do
        local category = { key = descriptor.key, name = descriptor.label, done = 0, total = 0, objectives = {} }
        categories[#categories + 1], byKey[descriptor.key] = category, category
    end

    local achievements, seen = {}, {}
    for _, achievementID in ipairs(definition.achievements or {}) do
        achievements[#achievements + 1], seen[achievementID] = achievementID, true
    end
    local storyAchievementID = C_QuestLog.GetZoneStoryInfo(mapID)
    if storyAchievementID and not seen[storyAchievementID] then
        achievements[#achievements + 1] = storyAchievementID
    end

    if zoneEligible then
        for _, id in ipairs(achievements) do ScanAchievement(byKey.achievements, id) end
        for _, entry in ipairs(definition.quests or {}) do
            if IsEligible(entry, definition) then ScanQuest(byKey.quests, entry, definition.name) end
        end
        for _, entry in ipairs(definition.mounts or {}) do
            if IsEligible(entry, definition) then ScanMount(byKey.mounts, entry) end
        end
        for _, entry in ipairs(definition.pets or {}) do
            if IsEligible(entry, definition) then ScanPet(byKey.pets, entry, definition.name) end
        end
        for _, entry in ipairs(definition.decor or {}) do
            if IsEligible(entry, definition) then ScanDecor(byKey.decor, entry, definition.name) end
        end
        local rareSources = {}
        for _, rare in ipairs(definition.rares or {}) do
            if rare.sourceID then rareSources[rare.sourceID] = true end
        end
        for _, entry in ipairs(definition.transmogs or {}) do
            local sourceID = EntryID(entry)
            if not rareSources[sourceID] and IsEligible(entry, definition) then
                ScanTransmog(byKey.transmogs, entry)
            end
        end
        for _, entry in ipairs(definition.rares or {}) do
            if IsEligible(entry, definition) then ScanRare(byKey.rares, entry) end
        end
    end

    local done, total = 0, 0
    for _, category in ipairs(categories) do done, total = done + category.done, total + category.total end
    self.ZoneData[mapID] = {
        mapID = mapID, name = definition.name, categories = categories,
        done = done, total = total, catalogComplete = definition.catalogComplete == true,
        eligible = zoneEligible,
        provider = "Built-in",
    }
    self:UpdateCompletionHistory(self.ZoneData[mapID])
    self:StoreZoneSummary(self.ZoneData[mapID])
    return self.ZoneData[mapID]
end

function ZL:ScanAllRouteZones()
    local keep = {}
    for _, mapID in ipairs(ZL_Saved.route or {}) do
        if not ZL_Saved.completedZones[mapID] then
            local data = self:ScanZone(mapID)
            if ZL_Saved.completedZones[mapID] then
                self.ZoneData[mapID] = nil
            else
                keep[mapID] = true
                break
            end
        end
    end

    self.forceLoadedZones = self.forceLoadedZones or {}
    local selectedMapID = self.selectedMapID
    if selectedMapID and self.forceLoadedZones[selectedMapID] then
        keep[selectedMapID] = true
        if not self.ZoneData[selectedMapID] then self:ScanZone(selectedMapID) end
    end

    for mapID in pairs(self.ZoneData) do
        if self:GetRoutePosition(mapID) and not keep[mapID] then self.ZoneData[mapID] = nil end
    end
end

function ZL:LoadCompletedZoneDetails(mapID, categoryKey)
    self.forceLoadedZones = self.forceLoadedZones or {}
    self.forceLoadedZones[mapID] = true
    self.ZoneData[mapID] = nil
    self:ScanZone(mapID)
    self.selectedMapID, self.selectedCategoryKey = mapID, categoryKey
    self._uiSignature = nil
    self.UI.scroll:SetVerticalScroll(0)
    self:RefreshUI()
end

function ZL:UnloadCompletedZoneDetails(mapID)
    if not mapID or not ZL_Saved.completedZones[mapID] then return end
    if self.forceLoadedZones then self.forceLoadedZones[mapID] = nil end
    self.ZoneData[mapID] = nil
end

function ZL:MarkTrackedItemCollected(itemID)
    if not itemID or ZL_Saved.collectedItems[itemID] then return end
    for mapID, definition in pairs(self.Catalog) do
        for _, rare in ipairs(definition.rares or {}) do
            if rare.itemID == itemID then
                ZL_Saved.collectedItems[itemID] = true
                self:ScheduleRefresh(0.1)
                return
            end
        end
    end
end
