-- ============================================================================
-- HDG_ExpansionData.lua - Centralized Expansion Data Module
-- ============================================================================
-- Single source of truth for all expansion-related data:
-- - Expansion ordering for display
-- - Expansion name normalization
-- - Expansion colors
-- - Expansion abbreviations
-- ============================================================================

HDG_ExpansionData = {}

-- ============================================================================
-- MASTER DATA TABLE (Single Source of Truth)
-- ============================================================================
-- Each expansion defined once with all its properties.
-- All lookup tables are built automatically from this data.
-- aliases: additional names that resolve to this expansion
--
-- IMPORTANT: DO NOT CHANGE the 'abbr' values! They are used as keys in:
--   - HDG_CraftQuotes.lua (profession craft quotes)
--   - HDG_Vamoose.lua (lumber farming quotes)
-- Changing abbreviations will break quote lookups.

-- Localized expansion names from WoW clients:
-- EN=English, DE=German, ES=Spanish, FR=French, IT=Italian, RU=Russian, KO=Korean, zhCN=Simplified Chinese, zhTW=Traditional Chinese
local EXPANSION_DATA = {
    -- Classic: Klassisch (DE), Clasico (ES), Classique (FR), Classico (IT), Clássico (ptBR)
    { display = "Classic",              api = "Classic",      abbr = "Classic", short = "CLS", color = { 0.6, 0.6, 0.6 },
      aliases = { "Klassisch", "Clasico", "Classique", "Classico", "Clássico", "Классика", "经典", "经典旧世", "經典", "经典版", "經典版", "클래식" } },
    -- TBC: Outland (EN), Scherbenwelt (DE), Terrallende (ES), Outreterre (FR), Terre Esterne (IT)
    { display = "The Burning Crusade",  api = "Outland",      abbr = "TBC",     short = "TBC", color = { 0.4, 0.7, 0.4 },
      apiTags = { "Burning Crusade", "Scherbenwelt", "Terrallende", "Outreterre", "Terre Esterne", "Terralém", "Запределье", "外域", "燃烧的远征", "燃燒的遠征", "아웃랜드", "불타는 성전" } },
    -- WotLK: Northrend (EN), Nordend (DE), Rasganorte (ES), Norfendre (FR), Nordania (IT)
    { display = "Wrath of the Lich King", api = "Northrend",  abbr = "WotLK",   short = "WLK", color = { 0.4, 0.6, 0.8 },
      aliases = { "WotLK", "Nordend", "Rasganorte", "Norfendre", "Nordania", "Nortúndria", "Нордскол", "诺森德", "巫妖王之怒", "北裂境", "노스렌드", "리치 왕의 분노" } },
    -- Cata: Cataclysme (FR), Kataklysmus (DE), Cataclismo (ES/ptBR), Cataclisma (IT)
    { display = "Cataclysm",            api = "Cataclysm",    abbr = "Cata",    short = "CAT", color = { 0.8, 0.5, 0.2 },
      aliases = { "Cataclysme", "Kataklysmus", "Cataclismo", "Cataclisma", "Катаклизм", "大地的裂变", "浩劫與重生", "대격변" } },
    -- MoP: Pandarie (FR), Pandaria same in most other languages
    { display = "Mists of Pandaria",    api = "Pandaria",     abbr = "MoP",     short = "MOP", color = { 0.3, 0.7, 0.5 },
      aliases = { "MoP", "Pandarie", "Pandária", "Пандария", "潘达利亚", "熊猫人之谜", "潘達利亞", "潘達利亞之謎", "판다리아", "판다리아의 안개" } },
    -- WoD: Draenor same in most languages
    { display = "Warlords of Draenor",  api = "Draenor",      abbr = "WoD",     short = "WOD", color = { 0.7, 0.5, 0.3 },
      aliases = { "WoD", "Дренор", "德拉诺", "德拉诺之王", "德拉諾", "德拉諾之霸", "드레노어", "드레노어의 전쟁군주" } },
    -- Legion: Légion (FR), same in most other languages
    { display = "Legion",               api = "Legion",       abbr = "Legion",  short = "LEG", color = { 0.5, 0.9, 0.3 },
      aliases = { "Légion", "Легион", "军团再临", "軍臨天下", "군단" } },
    -- BfA: Kul Tiran (Alliance), Zandalari (Horde) - faction-specific profession skill names
    -- German uses "Zandalar" (place name) not "Zandalari" (demonym) for Horde professions
    -- RU: "Кул-тирасская" (adj form in profession names), "Кул-Тирас" (noun); old alias "Культирас" kept for legacy SavedVars
    { display = "Battle for Azeroth",   api = "BfA",          abbr = "BfA",     short = "BFA", color = { 0.8, 0.7, 0.2 },
      aliases = { "Kul", "Kul Tiran", "Zandalari", "Zandalar", "Kul Tiras", "Культирас", "Кул-Тирас", "Кул-тирас", "Зандалари", "Зандалар", "库尔提拉斯", "争霸艾泽拉斯", "庫爾提拉斯", "決戰艾澤拉斯", "赞达拉", "贊達拉", "쿨 티란", "잔달라", "격전의 아제로스" } },
    -- SL: Schattenlande (DE), Ombreterre (FR), Tierras Sombrías (ES), Terre Ombrose (IT), Terras Sombrias (ptBR)
    { display = "Shadowlands",          api = "Shadowlands",  abbr = "SL",      short = "SL",  color = { 0.6, 0.4, 0.8 },
      aliases = { "Schattenlande", "Ombreterre", "Tierras Sombrías", "Terre Ombrose", "Terras Sombrias", "Темные Земли", "暗影国度", "暗影之境", "暗影界", "어둠땅" } },
    -- DF: Dracheninseln (DE), Islas del Dragón (ES), Îles aux Dragons (FR), Isole dei Draghi (IT)
    { display = "Dragonflight",         api = "Dragon Isles", abbr = "DF",      short = "DF",  color = { 0.2, 0.6, 1.0 },
      aliases = { "Dragon", "Dracheninseln", "Islas del Dragón", "Islas Dragón", "Îles aux Dragons", "Isole dei Draghi", "Ilhas do Dragão", "Драконьи острова", "巨龙群岛", "巨龙时代", "巨龍群島", "巨龍崛起", "용의 섬", "용군단" } },
    -- TWW: Khaz Algar is same in most languages (proper noun)
    -- RU: may appear as "Каз'Алгар", "Каз-Алгар", or "Каз Алгар" depending on client
    { display = "The War Within",       api = "Khaz Algar",   abbr = "TWW",     short = "TWW", color = { 1.0, 0.5, 0.2 },
      aliases = { "Khaz", "Каз'Алгар", "Каз-Алгар", "Каз-алгар", "Каз Алгар", "卡兹阿加", "地心之战", "卡茲阿爾加", "地心之戰", "카즈 알가르", "내부 전쟁" } },
    { display = "Midnight",             api = "Midnight",     abbr = "MN",      short = "MN",  color = { 0.6, 0.2, 0.8 },
      aliases = { "至暗之夜", "한밤" } },
}

-- ============================================================================
-- BUILD LOOKUP TABLES (Auto-generated from EXPANSION_DATA)
-- ============================================================================

-- Display name order for UI iteration
HDG_ExpansionData.EXPANSION_ORDER = {}

-- Indexed by any name format -> expansion data
local expansionByName = {}

-- Build all lookups from master data
for i, exp in ipairs(EXPANSION_DATA) do
    exp.order = i
    HDG_ExpansionData.EXPANSION_ORDER[i] = exp.display

    -- Index by all name formats (display, api, abbr)
    expansionByName[exp.display] = exp
    expansionByName[exp.api] = exp
    expansionByName[exp.abbr] = exp

    -- Index by aliases
    if exp.aliases then
        for _, alias in ipairs(exp.aliases) do
            expansionByName[alias] = exp
        end
    end

    -- Index by API tag names (Blizzard uses different names in catalog tags)
    if exp.apiTags then
        for _, apiTag in ipairs(exp.apiTags) do
            expansionByName[apiTag] = exp
        end
    end
end

-- Add unknown fallback
local UNKNOWN_EXPANSION = { display = "Unknown", api = "Unknown", abbr = "???", color = { 0.5, 0.5, 0.5 }, order = 999 }
expansionByName["Unknown"] = UNKNOWN_EXPANSION

-- ============================================================================
-- UTILITY FUNCTIONS
-- ============================================================================

-- Internal: resolve any name format to expansion data
local function resolveExpansion(name)
    if not name then return UNKNOWN_EXPANSION end
    return expansionByName[name] or UNKNOWN_EXPANSION
end

-- Get expansion sort order (works with any name format)
function HDG_ExpansionData.GetSortOrder(expansion)
    return resolveExpansion(expansion).order
end

-- ============================================================================
-- HOUSING CATALOG TAG-BASED EXPANSION RESOLUTION
-- ============================================================================
-- Reads dataTagsByID from a decor catalog row and returns the matching
-- expansion name. Uses C_HousingCatalog.GetAllFilterTagGroups() — same call
-- Blizzard's filter UI uses — so it picks up Classic, Midnight, and any
-- future expansions automatically with localized names.

local _expansionTagIDs

local function buildExpansionTagSet()
    if _expansionTagIDs then return _expansionTagIDs end
    if not (C_HousingCatalog and C_HousingCatalog.GetAllFilterTagGroups) then return nil end
    local groups = C_HousingCatalog.GetAllFilterTagGroups()
    if not groups then return nil end
    for _, group in ipairs(groups) do
        if (group.groupName or ""):find("Expansion") then
            local set = {}
            -- group.tags is an array of { tagID = N, tagName = "..." }; pairs()
            -- keys are sequential indices, NOT the real tag IDs.
            for _, tagInfo in pairs(group.tags or {}) do
                if tagInfo and tagInfo.tagID then set[tagInfo.tagID] = true end
            end
            _expansionTagIDs = set
            return set
        end
    end
end

-- Returns the expansion name (string) from a decor item's dataTagsByID map,
-- or nil when no expansion tag is present. Multiple expansion tags resolve to
-- the highest tag ID (newest), matching Blizzard's filter behaviour of
-- bucketing each item under exactly one expansion.
function HDG_ExpansionData.GetExpansionFromCatalogTags(tagsByID)
    if not tagsByID then return nil end
    local expTags = buildExpansionTagSet()
    if not expTags then return nil end
    local highest, exp = 0, nil
    for tid, tname in pairs(tagsByID) do
        if type(tid) == "number" and expTags[tid] and tid > highest then
            highest, exp = tid, tname
        end
    end
    return exp
end

-- Test/diagnostic hook so /reload picks up tag-set changes without process restart.
function HDG_ExpansionData.InvalidateExpansionTagCache()
    _expansionTagIDs = nil
end

-- Get expansion color (works with any name format)
function HDG_ExpansionData.GetColor(expansion)
    return resolveExpansion(expansion).color
end

-- Get abbreviation (works with any name format)
function HDG_ExpansionData.GetAbbreviation(expansion)
    return resolveExpansion(expansion).abbr
end

-- Get short name (max 3 chars, works with any name format)
function HDG_ExpansionData.GetShortName(expansion)
    return resolveExpansion(expansion).short
end

-- Get Blizzard catalog tag name (apiTags[1] if present, else api name)
function HDG_ExpansionData.GetBlizzardTag(expansion)
    local exp = resolveExpansion(expansion)
    if exp.apiTags and exp.apiTags[1] then return exp.apiTags[1] end
    return exp.api
end

-- Get full display name (works with any name format)
function HDG_ExpansionData.GetDisplayName(expansion)
    return resolveExpansion(expansion).display
end

-- Normalize API expansion name to display name for consistent storage
-- Handles faction-specific BfA names (Zandalari/Kul Tiran -> Battle for Azeroth)
-- Handles API names (Dragon Isles -> Dragonflight, Khaz Algar -> The War Within)
function HDG_ExpansionData.NormalizeApiName(apiName)
    if not apiName or apiName == "" then return UNKNOWN_EXPANSION.display end
    -- Direct lookup first (covers all indexed names/aliases)
    local direct = expansionByName[apiName]
    if direct then return direct.display end
    -- Fallback: substring match against api names, aliases, and apiTags
    for _, exp in ipairs(EXPANSION_DATA) do
        if string.find(apiName, exp.api, 1, true) then return exp.display end
        if exp.aliases then
            for _, alias in ipairs(exp.aliases) do
                if string.find(apiName, alias, 1, true) then return exp.display end
            end
        end
        if exp.apiTags then
            for _, tag in ipairs(exp.apiTags) do
                if string.find(apiName, tag, 1, true) then return exp.display end
            end
        end
    end
    -- Log unrecognized name to chat so user can report it
    print("|cFFFF6600[HDG]|r Unrecognized expansion name: '" .. tostring(apiName) .. "' - please report this in CurseForge comments!")
    return UNKNOWN_EXPANSION.display
end

-- Normalize expansion prefix to full display name
-- Used for converting "Outland Alchemy" -> "The Burning Crusade"
function HDG_ExpansionData.NormalizeToDisplayName(expansion)
    if not expansion then return "Unknown" end

    -- Try direct lookup first
    local resolved = expansionByName[expansion]
    if resolved then return resolved.display end

    -- Extract first word (e.g., "Classic" from "Classic Alchemy")
    local prefix = expansion:match("^([^%s]+)") or expansion
    resolved = expansionByName[prefix]
    if resolved then return resolved.display end

    return "Unknown"
end

-- Get expansion from skill name string
function HDG_ExpansionData.GetExpansionFromSkill(skillName)
    if not skillName then return "Unknown" end

    -- Check each expansion's API name, aliases, and apiTags in the skill string
    for _, exp in ipairs(EXPANSION_DATA) do
        if string.find(skillName, exp.api, 1, true) then
            return exp.display
        end
        -- Check aliases (e.g., "Kul Tiran", "Zandalari" for BfA, "Nordend" for WotLK German)
        if exp.aliases then
            for _, alias in ipairs(exp.aliases) do
                if string.find(skillName, alias, 1, true) then
                    return exp.display
                end
            end
        end
        -- Check apiTags (localized API names like "Scherbenwelt" for German TBC)
        if exp.apiTags then
            for _, tag in ipairs(exp.apiTags) do
                if string.find(skillName, tag, 1, true) then
                    return exp.display
                end
            end
        end
    end

    return "Unknown"
end

-- ============================================================================
-- LEGACY COMPATIBILITY (for existing code that accesses tables directly)
-- ============================================================================

-- Build EXPANSION_SORT_ORDER for any code that accesses it directly
HDG_ExpansionData.EXPANSION_SORT_ORDER = setmetatable({}, {
    __index = function(_, key)
        return resolveExpansion(key).order
    end
})

-- Build EXPANSION_COLORS for any code that accesses it directly
HDG_ExpansionData.EXPANSION_COLORS = setmetatable({}, {
    __index = function(_, key)
        return resolveExpansion(key).color
    end
})

-- Build EXPANSION_ABBR for any code that accesses it directly
HDG_ExpansionData.EXPANSION_ABBR = setmetatable({}, {
    __index = function(_, key)
        return resolveExpansion(key).abbr
    end
})

-- Build EXPANSION_SHORT for any code that accesses it directly (max 3 chars)
HDG_ExpansionData.EXPANSION_SHORT = setmetatable({}, {
    __index = function(_, key)
        return resolveExpansion(key).short
    end
})

-- ============================================================================
-- PROFESSION NAME LOCALIZATION
-- ============================================================================
-- Helper to get localized profession name from our localization table.
-- Uses addon's locale override setting, not just WoW client locale.
-- Falls back to WoW's name if no translation exists.

-- Mapping from English profession name to localization key
local PROF_LOCALE_KEYS = {
    ["All"] = "PROF_ALL",
    ["Alchemy"] = "PROF_ALCHEMY",
    ["Blacksmithing"] = "PROF_BLACKSMITHING",
    ["Cooking"] = "PROF_COOKING",
    ["Enchanting"] = "PROF_ENCHANTING",
    ["Engineering"] = "PROF_ENGINEERING",
    ["Inscription"] = "PROF_INSCRIPTION",
    ["Jewelcrafting"] = "PROF_JEWELCRAFTING",
    ["Leatherworking"] = "PROF_LEATHERWORKING",
    ["Tailoring"] = "PROF_TAILORING",
    ["Herbalism"] = "PROF_HERBALISM",
    ["Mining"] = "PROF_MINING",
    ["Skinning"] = "PROF_SKINNING",
    ["Fishing"] = "PROF_FISHING",
}

-- Cache for WoW API localized profession names
local wowProfessionCache = {}

-- Get localized profession name (uses addon locale override first, then WoW API)
-- @param englishName: English profession name (e.g., "Alchemy")
-- @return Localized name from our table, or WoW API, or English name
function HDG_ExpansionData.GetLocalizedProfessionName(englishName)
    if not englishName then return "Unknown" end

    local L = HDG_L or {}
    local key = PROF_LOCALE_KEYS[englishName]

    -- Check our addon's localization first (respects locale override)
    if key and L[key] then
        return L[key]
    end

    -- Fall back to WoW API localized name
    if not wowProfessionCache[englishName] then
        local skillLineID = HDG_ExpansionData.PROFESSION_IDS[englishName]
        if skillLineID and C_TradeSkillUI and C_TradeSkillUI.GetProfessionInfoBySkillLineID then
            local profInfo = C_TradeSkillUI.GetProfessionInfoBySkillLineID(skillLineID)
            if profInfo and profInfo.professionName then
                wowProfessionCache[englishName] = profInfo.professionName
            end
        end
    end

    if wowProfessionCache[englishName] then
        return wowProfessionCache[englishName]
    end

    -- Final fallback to English name
    return englishName
end

-- ============================================================================
-- PROFESSION DATA (Bidirectional Lookup)
-- ============================================================================

-- Master profession data (single source of truth)
-- Using Mobile atlases for all professions (clean icons with transparent backgrounds)
local PROFESSION_DATA = {
    -- Crafting professions (using Mobile atlases)
    { name = "Alchemy",       id = 171, code = "AL", atlas = "Mobile-Alchemy" },
    { name = "Blacksmithing", id = 164, code = "BS", atlas = "Mobile-Blacksmithing" },
    { name = "Cooking",       id = 185, code = "CK", atlas = "Mobile-Cooking" },
    { name = "Enchanting",    id = 333, code = "EN", atlas = "Mobile-Enchanting" },
    { name = "Engineering",   id = 202, code = "EG", atlas = "Mobile-Enginnering" },  -- Blizzard typo
    { name = "Inscription",   id = 773, code = "IN", atlas = "Mobile-Inscription" },
    { name = "Jewelcrafting", id = 755, code = "JC", atlas = "Mobile-Jewelcrafting" },
    { name = "Leatherworking",id = 165, code = "LW", atlas = "Mobile-Leatherworking" },
    { name = "Tailoring",     id = 197, code = "TL", atlas = "Mobile-Tailoring" },
    -- Gathering professions (using Mobile atlases)
    { name = "Herbalism",     id = 182, code = "HB", atlas = "Mobile-Herbalism" },
    { name = "Mining",        id = 186, code = "MN", atlas = "Mobile-Mining" },
    { name = "Skinning",      id = 393, code = "SK", atlas = "Mobile-Skinning" },
}

-- Build lookup tables from master data
HDG_ExpansionData.PROFESSION_IDS = {}    -- Name -> ID
HDG_ExpansionData.PROFESSION_NAMES = {}  -- ID -> Name
HDG_ExpansionData.PROFESSION_CODES = {}  -- Name -> Code (2-letter)
HDG_ExpansionData.PROFESSION_ICONS = {}  -- Name -> {atlas=...}
HDG_ExpansionData.PROFESSION_BY_CODE = {} -- Code -> Name

for _, prof in ipairs(PROFESSION_DATA) do
    HDG_ExpansionData.PROFESSION_IDS[prof.name] = prof.id
    HDG_ExpansionData.PROFESSION_NAMES[prof.id] = prof.name
    HDG_ExpansionData.PROFESSION_CODES[prof.name] = prof.code
    HDG_ExpansionData.PROFESSION_ICONS[prof.name] = { atlas = prof.atlas, icon = prof.icon }
    HDG_ExpansionData.PROFESSION_BY_CODE[prof.code] = prof.name
end

-- Get profession ID from name
function HDG_ExpansionData.GetProfessionID(professionName)
    return HDG_ExpansionData.PROFESSION_IDS[professionName]
end

-- Get profession name from skill line ID
function HDG_ExpansionData.GetProfessionName(skillLineID)
    return HDG_ExpansionData.PROFESSION_NAMES[skillLineID]
end

-- Get profession 2-letter code from name
function HDG_ExpansionData.GetProfessionCode(professionName)
    return HDG_ExpansionData.PROFESSION_CODES[professionName] or "??"
end

-- Get profession name from 2-letter code
function HDG_ExpansionData.GetProfessionByCode(code)
    return HDG_ExpansionData.PROFESSION_BY_CODE[code]
end

-- Get profession icon data from name
-- Returns: iconOrAtlas, isAtlas (true if atlas, false if texture path)
function HDG_ExpansionData.GetProfessionIcon(professionName)
    local data = HDG_ExpansionData.PROFESSION_ICONS[professionName]
    if data then
        if data.atlas then
            return data.atlas, true
        elseif data.icon then
            return data.icon, false
        end
    end
    return "Interface\\Icons\\INV_Misc_QuestionMark", false
end

-- Apply profession icon to a texture element (handles both atlas and texture)
function HDG_ExpansionData.ApplyProfessionIcon(texture, professionName)
    local iconOrAtlas, isAtlas = HDG_ExpansionData.GetProfessionIcon(professionName)
    if isAtlas then
        texture:SetAtlas(iconOrAtlas)
    else
        texture:SetTexture(iconOrAtlas)
    end
end

-- ============================================================================
-- LUMBER DATA (Expansion-specific lumber types)
-- ============================================================================

-- Master lumber data: ordered array (single source of truth)
-- Order matches expansion order for consistent UI display
-- shortName is the material name only (no "Lumber" suffix) for language-neutral display
local LUMBER_DATA = {
    { id = 245586, name = "Ironwood Lumber",     shortName = "Ironwood",    expansion = "Classic",                achieveID = 62357 },
    { id = 242691, name = "Olemba Lumber",       shortName = "Olemba",      expansion = "The Burning Crusade",    achieveID = 62358 },
    { id = 251762, name = "Coldwind Lumber",     shortName = "Coldwind",    expansion = "Wrath of the Lich King", achieveID = 62359 },
    { id = 251764, name = "Ashwood Lumber",      shortName = "Ashwood",     expansion = "Cataclysm",             achieveID = 62360 },
    { id = 251763, name = "Bamboo Lumber",       shortName = "Bamboo",      expansion = "Mists of Pandaria",     achieveID = 62361 },
    { id = 251766, name = "Shadowmoon Lumber",   shortName = "Shadowmoon",  expansion = "Warlords of Draenor",   achieveID = 62362 },
    { id = 251767, name = "Fel-Touched Lumber",  shortName = "Fel-Touched", expansion = "Legion",                achieveID = 62363 },
    { id = 251768, name = "Darkpine Lumber",     shortName = "Darkpine",    expansion = "Battle for Azeroth",    achieveID = 62364 },
    { id = 251772, name = "Arden Lumber",        shortName = "Arden",       expansion = "Shadowlands",           achieveID = 62365 },
    { id = 251773, name = "Dragonpine Lumber",   shortName = "Dragonpine",  expansion = "Dragonflight",          achieveID = 62366 },
    { id = 248012, name = "Dornic Fir Lumber",   shortName = "Dornic Fir",  expansion = "The War Within",        achieveID = 62369 },
    { id = 256963, name = "Thalassian Lumber",   shortName = "Thalassian",  expansion = "Midnight",              achieveID = 62370 },
}

-- Build lookup tables from master data
HDG_ExpansionData.LUMBER_ORDER = {}  -- IDs in expansion order
HDG_ExpansionData.LUMBER_TYPES = {}  -- ID -> {name, expansion}
HDG_ExpansionData.LUMBER_IDS = {}    -- ID -> true (quick lookup)

for _, lumber in ipairs(LUMBER_DATA) do
    HDG_ExpansionData.LUMBER_ORDER[#HDG_ExpansionData.LUMBER_ORDER + 1] = lumber.id
    HDG_ExpansionData.LUMBER_TYPES[lumber.id] = { name = lumber.name, shortName = lumber.shortName, expansion = lumber.expansion, achieveID = lumber.achieveID }
    HDG_ExpansionData.LUMBER_IDS[lumber.id] = true
end

-- Check if an item ID is lumber
function HDG_ExpansionData.IsLumber(itemID)
    return HDG_ExpansionData.LUMBER_IDS[itemID] == true
end

-- Get lumber info by item ID
function HDG_ExpansionData.GetLumberInfo(itemID)
    return HDG_ExpansionData.LUMBER_TYPES[itemID]
end

-- Query mounted-harvesting achievement progress for a lumber type
-- Returns: quantity, reqQuantity, completed (or nil if no achieveID)
function HDG_ExpansionData.GetLumberAchievementProgress(lumberId)
    local info = HDG_ExpansionData.LUMBER_TYPES[lumberId]
    if not info or not info.achieveID then return nil end
    local _, _, _, completed = GetAchievementInfo(info.achieveID)
    local ok, _, _, _, quantity, reqQuantity = pcall(GetAchievementCriteriaInfo, info.achieveID, 1)
    if not ok or quantity == nil then return nil end
    return quantity, reqQuantity or 250, completed
end

-- Lumber/wood words to strip from localized names (various languages)
-- Patterns are applied in order: prefixes first, then suffixes
local LUMBER_WORD_PATTERNS = {
    -- Prefix patterns (word at start, strip with following space/preposition)
    -- More specific patterns (with preposition) must come before simpler ones
    "^Madeira%s+de%s+",     -- Portuguese: "Madeira de X"
    "^Madeira%s+",          -- Portuguese: "Madeira X" (without de)
    "^Madera%s+de%s+",      -- Spanish: "Madera de X"
    "^Madera%s+",           -- Spanish: "Madera X" (without de)
    "^Bois%s+d'",           -- French: "Bois d'X"
    "^Bois%s+de%s+",        -- French: "Bois de X"
    "^Bois%s+",             -- French: "Bois X" (without de)
    "^Legname%s+di%s+",     -- Italian: "Legname di X" (alternate word)
    "^Legname%s+",          -- Italian: "Legname X"
    "^Legno%s+di%s+",       -- Italian: "Legno di X"
    "^Legno%s+",            -- Italian: "Legno X" (without di)
    "^Holz%s+von%s+",       -- German: "Holz von X"
    "^Holz%s+",             -- German: "Holz X"
    "^Bauholz%s+",          -- German: "Bauholz X" (construction wood)
    "^Древесина%s+",        -- Russian: "Древесина X"
    "^Пиломатериалы%s+",    -- Russian: "Пиломатериалы X" (lumber)
    "^목재%s*",              -- Korean: "목재 X" or "목재X"
    "^木材%s*",              -- Chinese: "木材 X" or "木材X"
    -- Suffix patterns (word at end)
    "%s+Lumber$",           -- English: "X Lumber"
    "%s+Wood$",             -- English: "X Wood"
    "%-Holz$",              -- German: "X-Holz"
    "[Hh]olz$",             -- German: "Xholz" (compound)
    "%-[Bb]auholz$",        -- German: "X-Bauholz"
    "%s+목재$",              -- Korean: "X 목재"
    "목재$",                 -- Korean: "X목재" (no space)
    "%s*木材$",              -- Chinese: "X木材" or "X 木材"
    "%s+древесина$",        -- Russian: "X древесина"
    "%s+Madera$",           -- Spanish suffix form
    "%s+Madeira$",          -- Portuguese suffix form
    "%s+Bois$",             -- French suffix form
    "%s+Legno$",            -- Italian suffix form
    "%s+Legname$",          -- Italian suffix form (alternate)
}

-- Extract just the material name from a localized lumber name
-- e.g., "Ironwood Lumber" -> "Ironwood", "Madeira de Xiloferrea" -> "Xiloferrea"
function HDG_ExpansionData.GetLumberMaterialName(lumberId)
    if not lumberId then return nil end

    -- Get the localized name from WoW API
    local localizedName = C_Item and C_Item.GetItemNameByID(lumberId)
    if not localizedName then
        -- Fallback to English shortName if API not ready
        local info = HDG_ExpansionData.LUMBER_TYPES[lumberId]
        return info and info.shortName or nil
    end

    -- Apply patterns to strip lumber/wood words
    local materialName = localizedName
    for _, pattern in ipairs(LUMBER_WORD_PATTERNS) do
        local stripped = materialName:gsub(pattern, "")
        if stripped ~= materialName then
            materialName = stripped
            break  -- Only apply first matching pattern
        end
    end

    -- Trim any remaining whitespace
    materialName = materialName:match("^%s*(.-)%s*$")

    -- Capitalize first letter (handles lowercase after stripping prefix like "Madera xiloférrea" -> "Xiloférrea")
    if materialName and #materialName > 0 then
        materialName = materialName:sub(1, 1):upper() .. materialName:sub(2)
    end

    -- If nothing was stripped, return the full name (unknown language pattern)
    return materialName ~= "" and materialName or localizedName
end

-- ============================================================================
-- HOUSING ACHIEVEMENTS (Category 15606)
-- ============================================================================
-- Three groups: Decor Collection (11), Community Coupons (7), Lumber Harvesting (12)
-- Lumber achievements are already tracked per-type in LUMBER_DATA above.

-- Decor Collection achievements (unique uncommon+ decor collected)
local DECOR_ACHIEVE_DATA = {
    { id = 61308, threshold = 1 },
    { id = 61309, threshold = 5 },
    { id = 61310, threshold = 25 },
    { id = 61311, threshold = 50 },
    { id = 61312, threshold = 100 },
    { id = 61313, threshold = 150 },
    { id = 61314, threshold = 200 },
    { id = 61315, threshold = 250 },
    { id = 61316, threshold = 300 },
    { id = 61317, threshold = 400 },
    { id = 61318, threshold = 500 },
}

-- Community Coupon achievements (cumulative coupons earned)
local COUPON_ACHIEVE_DATA = {
    { id = 62371, threshold = 50 },    -- Couponing for Beginners
    { id = 62373, threshold = 250 },   -- Coupon Collector
    { id = 62374, threshold = 500 },   -- You Get The Best Deals Anywhere
    { id = 62375, threshold = 1000 },  -- Buying in Bulk
    { id = 62376, threshold = 2000 },  -- Extreme Couponing
    { id = 62377, threshold = 5000 },  -- A Fist Full of Coupons
    { id = 62378, threshold = 10000 }, -- A Few Coupons More
}

-- Expose for DataTab UI
HDG_ExpansionData.DECOR_ACHIEVEMENTS = DECOR_ACHIEVE_DATA
HDG_ExpansionData.COUPON_ACHIEVEMENTS = COUPON_ACHIEVE_DATA

-- O(1) lookup for all 30 housing achievement IDs
local HOUSING_ACHIEVE_IDS = {}
for _, a in ipairs(DECOR_ACHIEVE_DATA) do HOUSING_ACHIEVE_IDS[a.id] = true end
for _, a in ipairs(COUPON_ACHIEVE_DATA) do HOUSING_ACHIEVE_IDS[a.id] = true end
for _, lumber in ipairs(LUMBER_DATA) do
    if lumber.achieveID then HOUSING_ACHIEVE_IDS[lumber.achieveID] = true end
end

-- Check if an achievement ID belongs to housing (category 15606)
function HDG_ExpansionData.IsHousingAchievement(achieveID)
    return HOUSING_ACHIEVE_IDS[achieveID] == true
end

-- Get decor collection achievement progress
-- Returns: currentQty, nextThreshold, totalCompleted, totalAchievements
function HDG_ExpansionData.GetDecorAchievementProgress()
    local completed = 0
    local currentQty = 0
    local nextThreshold = nil
    for _, a in ipairs(DECOR_ACHIEVE_DATA) do
        local ok, _, _, _, done = pcall(GetAchievementInfo, a.id)
        if not ok then
            -- Achievement doesn't exist in this build; skip
        elseif done then
            completed = completed + 1
        elseif not nextThreshold then
            local cOk, _, _, _, qty = pcall(GetAchievementCriteriaInfo, a.id, 1)
            currentQty = (cOk and qty) or 0
            nextThreshold = a.threshold
        end
    end
    if not nextThreshold then
        currentQty = DECOR_ACHIEVE_DATA[#DECOR_ACHIEVE_DATA].threshold
        nextThreshold = currentQty
    end
    return currentQty, nextThreshold, completed, #DECOR_ACHIEVE_DATA
end

-- Get community coupon achievement progress
-- Returns: currentQty, nextThreshold, totalCompleted, totalAchievements
function HDG_ExpansionData.GetCouponAchievementProgress()
    local completed = 0
    local currentQty = 0
    local nextThreshold = nil
    for _, a in ipairs(COUPON_ACHIEVE_DATA) do
        local ok, _, _, _, done = pcall(GetAchievementInfo, a.id)
        if not ok then
            -- skip
        elseif done then
            completed = completed + 1
        elseif not nextThreshold then
            local cOk, _, _, _, qty = pcall(GetAchievementCriteriaInfo, a.id, 1)
            currentQty = (cOk and qty) or 0
            nextThreshold = a.threshold
        end
    end
    if not nextThreshold then
        currentQty = COUPON_ACHIEVE_DATA[#COUPON_ACHIEVE_DATA].threshold
        nextThreshold = currentQty
    end
    return currentQty, nextThreshold, completed, #COUPON_ACHIEVE_DATA
end

-- Get lumber achievement summary (how many of 12 completed)
-- Returns: completedCount, totalCount
function HDG_ExpansionData.GetLumberAchievementSummary()
    local completed = 0
    local total = 0
    for _, lumber in ipairs(LUMBER_DATA) do
        if lumber.achieveID then
            local ok, _, _, _, done = pcall(GetAchievementInfo, lumber.achieveID)
            if ok then
                total = total + 1
                if done then completed = completed + 1 end
            end
        end
    end
    return completed, total
end

-- ============================================================================
-- BOP GATHERED REAGENTS (not tradeable, exclude from cost calculations)
-- ============================================================================

-- BoP gathered items that can't be bought on AH
HDG_ExpansionData.BOP_REAGENT_IDS = {
    [120945] = true,  -- Primal Spirit (WoD)
}

-- Check if an item ID is a BoP gathered reagent
function HDG_ExpansionData.IsBoPReagent(itemID)
    -- Check lumber first
    if HDG_ExpansionData.LUMBER_IDS[itemID] then
        return true
    end
    -- Check other BoP reagents
    return HDG_ExpansionData.BOP_REAGENT_IDS[itemID] == true
end

-- ============================================================================
-- DECOR CURRENCY EXPANSION OVERRIDES
-- ============================================================================
-- Currencies are introduced in a specific expansion. The aggregator's vendor
-- heuristic (earliest vendor.exp wins) buckets currencies wrong when capital-
-- city vendors in Classic zones (Stormwind, Orgrimmar) accept newer currencies.
-- This table is consulted FIRST so introduction-era is correct regardless of
-- which vendors currently accept the currency.

local CURRENCY_EXPANSION_OVERRIDES = {
    -- Midnight (33xx range)
    [3316] = "Midnight",  -- Voidlight Marl
    [3363] = "Midnight",
    [3373] = "Midnight",  -- Angler Pearls
    [3377] = "Midnight",
    [3379] = "Midnight",  -- Brimming Arcana
    [3392] = "Midnight",
    [3393] = "Midnight",  -- Illusionary Coin
}

function HDG_ExpansionData.GetCurrencyExpansion(currencyID)
    return CURRENCY_EXPANSION_OVERRIDES[currencyID]
end

-- ============================================================================
-- ZONE DATA — single source of truth for zone names, map IDs, and expansions
-- String keys: "Zone Name" = { mapID, "Expansion" }  → populates both lookups
-- Integer keys: [mapID] = "Expansion"                 → expansion-only (no zone name)
-- ============================================================================

local ZONE_DATA = {
    -- Classic — Eastern Kingdoms
    ["Stormwind City"] = { 84, "Classic" },
    ["Ironforge"] = { 87, "Classic" },
    ["Undercity"] = { 90, "Classic" },
    ["Dun Morogh"] = { 27, "Classic" },
    ["Duskwood"] = { 47, "Classic" },
    ["Wetlands"] = { 56, "Classic" },
    ["Elwynn Forest"] = { 37, "Classic" },
    ["Westfall"] = { 52, "Classic" },
    ["Loch Modan"] = { 48, "Classic" },
    ["Blasted Lands"] = { 17, "Classic" },
    ["Burning Steppes"] = { 36, "Classic" },
    ["Eastern Plaguelands"] = { 23, "Classic" },
    ["Northern Stranglethorn"] = { 50, "Classic" },
    ["Deeprun Tram"] = { 499, "Classic" },
    -- Classic — Kalimdor
    ["Orgrimmar"] = { 85, "Classic" },
    ["Thunder Bluff"] = { 88, "Classic" },
    ["Darnassus"] = { 89, "Classic" },
    ["Mulgore"] = { 7, "Classic" },
    ["Dustwallow Marsh"] = { 70, "Classic" },
    ["Felwood"] = { 77, "Classic" },
    ["Taldrassil"] = { 57, "Classic" },
    -- Classic — instances
    ["Blackrock Depths"] = { 242, "Classic" },
    ["Shadowfang Keep"] = { 310, "Classic" },
    ["Upper Blackrock Spire"] = { 300, "Classic" },
    -- Classic — mapID-only (continents, zones without vendor name matches)
    [1] = "Classic", [12] = "Classic", [13] = "Classic",
    [14] = "Classic", [15] = "Classic", [18] = "Classic",
    [22] = "Classic", [25] = "Classic", [26] = "Classic",
    [32] = "Classic", [42] = "Classic", [49] = "Classic",
    [51] = "Classic", [56] = "Classic", [62] = "Classic",
    [63] = "Classic", [64] = "Classic", [65] = "Classic",
    [66] = "Classic", [69] = "Classic", [71] = "Classic",
    [76] = "Classic", [78] = "Classic", [80] = "Classic",
    [81] = "Classic", [83] = "Classic", [97] = "Classic",

    -- TBC
    ["Silvermoon City"] = { 2393, "Midnight" },
    ["Exodar"] = { 103, "The Burning Crusade" },
    ["Eversong Woods"] = { 94, "The Burning Crusade" },
    ["Ghostlands"] = { 95, "The Burning Crusade" },
    ["Nagrand"] = { 107, "The Burning Crusade" },
    ["Shadowmoon Valley"] = { 104, "The Burning Crusade" },
    ["Isle of Quel'Danas"] = { 122, "The Burning Crusade" },
    ["Karazhan"] = { 350, "The Burning Crusade" },
    [100] = "The Burning Crusade", [101] = "The Burning Crusade",
    [102] = "The Burning Crusade", [105] = "The Burning Crusade",
    [108] = "The Burning Crusade", [109] = "The Burning Crusade",

    -- WotLK
    ["Borean Tundra"] = { 114, "Wrath of the Lich King" },
    ["Dalaran"] = { 627, "Wrath of the Lich King" },
    ["Grizzly Hills"] = { 116, "Wrath of the Lich King" },
    ["Sholazar Basin"] = { 119, "Wrath of the Lich King" },
    ["Pit of Saron"] = { 184, "Wrath of the Lich King" },
    [115] = "Wrath of the Lich King", [117] = "Wrath of the Lich King",
    [118] = "Wrath of the Lich King", [120] = "Wrath of the Lich King",
    [121] = "Wrath of the Lich King",

    -- Cataclysm
    ["Twilight Highlands"] = { 241, "Cataclysm" },
    ["Ruins of Gilneas"] = { 217, "Cataclysm" },
    ["Gilneas"] = { 217, "Cataclysm" },
    ["Searing Gorge"] = { 224, "Cataclysm" },
    ["Silverpine Forest"] = { 21, "Cataclysm" },
    [198] = "Cataclysm", [203] = "Cataclysm", [204] = "Cataclysm",
    [207] = "Cataclysm", [244] = "Cataclysm",

    -- MoP
    ["Kun-Lai Summit"] = { 379, "Mists of Pandaria" },
    ["The Jade Forest"] = { 371, "Mists of Pandaria" },
    ["Vale of Eternal Blossoms"] = { 390, "Mists of Pandaria" },
    ["Valley of the Four Winds"] = { 376, "Mists of Pandaria" },
    ["Timeless Isle"] = { 554, "Mists of Pandaria" },
    ["Brawl'gar Arena"] = { 503, "Mists of Pandaria" },
    ["Siege of Orgrimmar"] = { 556, "Mists of Pandaria" },
    ["Temple of the Jade Serpent"] = { 867, "Mists of Pandaria" },
    ["Zul'Aman"] = { 333, "Mists of Pandaria" },
    [388] = "Mists of Pandaria", [418] = "Mists of Pandaria",
    [422] = "Mists of Pandaria", [504] = "Mists of Pandaria",

    -- WoD
    ["Stormshield"] = { 622, "Warlords of Draenor" },
    ["Warspear"] = { 624, "Warlords of Draenor" },
    ["Lunarfall"] = { 582, "Warlords of Draenor" },
    ["Frostwall"] = { 590, "Warlords of Draenor" },
    ["Spires of Arak"] = { 542, "Warlords of Draenor" },
    ["Talador"] = { 535, "Warlords of Draenor" },
    ["Frostfire Ridge"] = { 525, "Warlords of Draenor" },
    ["Tanaan Jungle"] = { 534, "Warlords of Draenor" },
    ["Iron Docks"] = { 595, "Warlords of Draenor" },
    ["Auchindoun"] = { 593, "Warlords of Draenor" },
    [539] = "Warlords of Draenor", [543] = "Warlords of Draenor",
    [550] = "Warlords of Draenor",

    -- Legion
    ["Val'sharah"] = { 641, "Legion" },
    ["Suramar"] = { 680, "Legion" },
    ["Highmountain"] = { 650, "Legion" },
    ["Azsuna"] = { 630, "Legion" },
    ["Dreadscar Rift"] = { 717, "Legion" },
    ["Skyhold"] = { 695, "Legion" },
    ["Netherlight Temple"] = { 702, "Legion" },
    ["Hall of the Guardian"] = { 734, "Legion" },
    ["The Dreamgrove"] = { 747, "Legion" },
    ["Trueshot Lodge"] = { 739, "Legion" },
    ["The Maelstrom"] = { 726, "Legion" },
    ["The Fel Hammer"] = { 720, "Legion" },
    ["The Vindicaar"] = { 940, "Legion" },
    ["Krokuun"] = { 830, "Legion" },
    ["The Hall of Shadows"] = { 711, "Legion" },
    ["Mardum, the Shattered Abyss"] = { 672, "Legion" },
    ["Acherus: The Ebon Hold"] = { 648, "Legion" },
    ["Sanctum of Light"] = { 24, "Legion" },
    ["Court of Stars"] = { 761, "Legion" },
    ["Darkheart Thicket"] = { 733, "Legion" },
    ["Neltharion's Lair"] = { 731, "Legion" },
    ["The Nighthold"] = { 764, "Legion" },
    ["Seat of the Triumvirate"] = { 903, "Legion" },
    [619] = "Legion", [634] = "Legion", [646] = "Legion",

    -- BfA
    ["Zuldazar"] = { 862, "Battle for Azeroth" },
    ["Nazmir"] = { 863, "Battle for Azeroth" },
    ["Tiragarde Sound"] = { 895, "Battle for Azeroth" },
    ["Chamber of Heart"] = { 1473, "Battle for Azeroth" },
    ["Mechagon"] = { 1462, "Battle for Azeroth" },
    ["Vol'dun"] = { 864, "Battle for Azeroth" },
    ["Drustvar"] = { 896, "Battle for Azeroth" },
    ["Stormsong Valley"] = { 942, "Battle for Azeroth" },
    ["Freehold"] = { 936, "Battle for Azeroth" },
    ["Shrine of the Storm"] = { 1036, "Battle for Azeroth" },
    [1161] = "Battle for Azeroth", [1355] = "Battle for Azeroth",

    -- Shadowlands
    ["Revendreth"] = { 1525, "Shadowlands" },
    ["The Maw"] = { 1543, "Shadowlands" },
    [1533] = "Shadowlands", [1536] = "Shadowlands",
    [1565] = "Shadowlands", [1550] = "Shadowlands",

    -- Dragonflight
    ["Valdrakken"] = { 2112, "Dragonflight" },
    ["The Waking Shores"] = { 2022, "Dragonflight" },
    ["Thaldraszus"] = { 2025, "Dragonflight" },
    ["The Azure Span"] = { 2024, "Dragonflight" },
    ["The Forbidden Reach"] = { 2151, "Dragonflight" },
    ["Amirdrassil"] = { 2232, "Dragonflight" },
    ["Neltharus"] = { 2080, "Dragonflight" },
    ["Ruby Life Pools"] = { 2095, "Dragonflight" },
    ["Algeth'ar Academy"] = { 2097, "Dragonflight" },
    [2023] = "Dragonflight", [2133] = "Dragonflight", [2200] = "Dragonflight",

    -- TWW
    ["Dornogal"] = { 2339, "The War Within" },
    ["Hallowfall"] = { 2215, "The War Within" },
    ["The Ringing Deeps"] = { 2214, "The War Within" },
    ["Isle of Dorn"] = { 2248, "The War Within" },
    ["City of Threads"] = { 2255, "The War Within" },
    ["Priory of the Sacred Flame"] = { 2649, "The War Within" },
    ["Cinderbrew Meadery"] = { 2661, "The War Within" },
    ["Collegiate Calamity"] = { 2653, "The War Within" },
    ["Deephaul Ravine"] = { 2601, "The War Within" },
    [2256] = "The War Within", [2735] = "The War Within",
    [2736] = "The War Within", [2769] = "The War Within",
    [2771] = "The War Within", [2706] = "The War Within",
    [2694] = "The War Within",

    -- Midnight
    ["Undermine"] = { 2346, "Midnight" },
    ["Razorwind Shores"] = { 2351, "Midnight" },
    ["Founder's Point"] = { 2352, "Midnight" },
    ["K'aresh"] = { 2371, "Midnight" },
    ["Voidstorm"] = { 2405, "Midnight" },
    ["Arcantina"] = { 2541, "Midnight" },
    ["Harandar"] = { 2413, "Midnight" },
    ["Liberation of Undermine"] = { 2346, "Midnight" },
    ["Windrunner Spire"] = { 2413, "Midnight" },
}

-- Built at load time from ZONE_DATA
HDG_ExpansionData.ZONE_MAP_IDS = {}
local MAP_TO_EXPANSION = {}

for key, val in pairs(ZONE_DATA) do
    if type(key) == "string" then
        -- Zone entry: "Name" = { mapID, "Expansion" }
        HDG_ExpansionData.ZONE_MAP_IDS[key] = val[1]
        MAP_TO_EXPANSION[val[1]] = val[2]
    else
        -- MapID-only entry: [mapID] = "Expansion"
        MAP_TO_EXPANSION[key] = val
    end
end

function HDG_ExpansionData.GetMapIDForZone(zoneName)
    return HDG_ExpansionData.ZONE_MAP_IDS[zoneName]
end

-- Resolve mapID to expansion, walking parent hierarchy if needed
function HDG_ExpansionData.GetExpansionForMapID(mapID)
    if not mapID or mapID == 0 then return "Other" end
    local exp = MAP_TO_EXPANSION[mapID]
    if exp then return exp end
    local currentID = mapID
    for _ = 1, 5 do
        local info = C_Map and C_Map.GetMapInfo and C_Map.GetMapInfo(currentID)
        if not info or not info.parentMapID or info.parentMapID == 0 then break end
        currentID = info.parentMapID
        exp = MAP_TO_EXPANSION[currentID]
        if exp then return exp end
    end
    return "Other"
end
