local addonName, privateDB = ...
local HDG = _G.HousingDecorGuide

-- Module namespace
HDG_Data = {}

-- ============================================================================
-- STORE HELPERS
-- ============================================================================

local function GetStore()
    local hdg = _G.HousingDecorGuide
    return hdg and hdg.Store
end

local function GetState()
    local store = GetStore()
    return store and store:GetState()
end

local function Dispatch(action, payload)
    local store = GetStore()
    if store then
        store:Dispatch(action, payload)
    end
end

local function GetCharacters()
    local state = GetState()
    return state and state.characters or {}
end

local function GetConfig(key)
    local state = GetState()
    return state and state.config and state.config[key]
end

local function GetUIState(key)
    local state = GetState()
    return state and state.ui and state.ui[key]
end

-- Recipe filter state (session-only, not persisted)
local recipeSearchFilter = ""
local recipeLowerNameCache = {}
local function GetRecipeLowerName(recipe)
    local key = recipe.recipeKey or recipe.name
    if not key then return "" end
    if not recipeLowerNameCache[key] then
        recipeLowerNameCache[key] = (recipe.name or ""):lower()
    end
    return recipeLowerNameCache[key]
end

-- Local references for speed
local C_Item = C_Item
local C_Timer = C_Timer
local C_TradeSkillUI = C_TradeSkillUI
local C_SpellBook = C_SpellBook
local time = time

-- Caching tables
local itemNameCache = {}
local itemNameLowerCache = {} -- pre-lowercased names for search
local collectionCache = {}
local collectionGeneration = 1 -- incremented on MarkDecorCollected to invalidate negative cache
local knownRecipeSpells = {}
local recipeSpellsBuilt = false
local pendingRefresh = false
local pendingRefreshTimer = nil
local itemLoadTimes = {} -- [itemID] = GetTime() when first requested
local UNRELEASED_TIMEOUT = 8 -- seconds before "Loading..." becomes "Unreleased"

-- Lumber data (reference centralized HDG_ExpansionData module)
local LUMBER_TYPES = HDG_ExpansionData.LUMBER_TYPES
HDG_Data.LUMBER_TYPES = LUMBER_TYPES

-- ============================================================================
-- HELPER FUNCTIONS
-- ============================================================================
-- Note: Expansion-related functions (GetExpansionColor, GetExpansionAbbr,
-- NormalizeExpansionName, GetExpansionFromSkill) have been moved to
-- HDG_ExpansionData module. Use HDG_ExpansionData directly.

-- Get cached item name
function HDG_Data.GetCachedItemName(itemID)
    if itemNameCache[itemID] then
        return itemNameCache[itemID], false
    end

    -- Try synchronous lookup first
    local itemName = C_Item.GetItemNameByID(itemID)
    if itemName then
        itemNameCache[itemID] = itemName
        itemNameLowerCache[itemID] = itemName:lower()
        return itemName, false
    end

    -- Check if we've been waiting too long (unreleased/future item)
    local now = GetTime()
    if itemLoadTimes[itemID] then
        if (now - itemLoadTimes[itemID]) > UNRELEASED_TIMEOUT then
            itemNameCache[itemID] = "Unreleased"
            itemNameLowerCache[itemID] = "unreleased"
            itemLoadTimes[itemID] = nil -- clear to prevent unbounded growth
            return "Unreleased", false
        end
    else
        itemLoadTimes[itemID] = now
    end

    -- Fall back to async loading
    local item = Item:CreateFromItemID(itemID)
    if not item:IsItemEmpty() then
        item:ContinueOnItemLoad(function()
            local name = item:GetItemName()
            if name then
                itemNameCache[itemID] = name
                itemNameLowerCache[itemID] = name:lower()
                itemLoadTimes[itemID] = nil
                -- Batch refresh (re-arm timer on each resolve)
                if privateDB.RefreshUI then
                    if pendingRefreshTimer then pendingRefreshTimer:Cancel() end
                    pendingRefreshTimer = C_Timer.NewTimer(0.5, function()
                        pendingRefreshTimer = nil
                        pendingRefresh = false
                        privateDB.RefreshUI()
                    end)
                    pendingRefresh = true
                end
            end
        end)
    end

    return "Loading...", true
end

-- Get localized item name, preferring WoW API over hardcoded DB name
-- This ensures names display in the user's client language
function HDG_Data.GetLocalizedName(itemID, fallbackName)
    if not itemID then
        return fallbackName or "Unknown"
    end

    local localizedName, isLoading = HDG_Data.GetCachedItemName(itemID)

    -- If we got a real name from the API, use it
    if localizedName and localizedName ~= "Loading..." then
        return localizedName
    end

    -- Still loading - use fallback but trigger async load
    return fallbackName or ("Item #" .. itemID)
end

-- Get pre-lowercased cached item name (for search filtering)
function HDG_Data.GetCachedItemNameLower(itemID)
    if itemNameLowerCache[itemID] then return itemNameLowerCache[itemID] end
    -- Trigger normal cache population
    local name = HDG_Data.GetCachedItemName(itemID)
    return itemNameLowerCache[itemID] or (name and name:lower()) or ""
end

-- Check if item is collected (by itemID)
-- Caches both positive and negative results; negatives are invalidated by generation counter
function HDG_Data.IsItemCollected(itemID)
    if not itemID then return false end

    local cached = collectionCache[itemID]
    -- Positive cache: permanent
    if cached == true then return true end
    -- Negative cache: valid if generation matches
    if cached and cached ~= true and cached == -collectionGeneration then return false end

    -- !!!! CRITICAL: DO NOT REMOVE the firstAcquisitionBonus == 0 check !!!!
    -- This is the ONLY reliable way to detect collected crafted items where the player
    -- has 0 quantity and 0 numPlaced (they learned the appearance but have no copies).
    -- Removing it breaks collection tracking for the majority of items in the addon.
    -- Known trade-off: promotion/achievement items have bonus 0 even when uncollected,
    -- causing false-positive "collected" for those source types. Fix this at the DISPLAY
    -- layer (e.g. AcqRows) by checking source type, NOT by altering this function.
    if C_HousingCatalog and C_HousingCatalog.GetCatalogEntryInfoByItem then
        local info = C_HousingCatalog.GetCatalogEntryInfoByItem(itemID, true)
        if info and ((info.quantity or 0) > 0 or (info.numPlaced or 0) > 0 or (info.firstAcquisitionBonus or -1) == 0) then
            collectionCache[itemID] = true
            return true
        end
    end

    -- Fallback: decorID from AllDecorDB (avoids O(n) decorItem scan)
    local decorID
    if HDG_AllDecorDB and HDG_AllDecorDB[itemID] then
        decorID = HDG_AllDecorDB[itemID][1]
    elseif privateDB.decorItem[itemID] then
        decorID = privateDB.decorItem[itemID].decorID
    end
    if decorID and C_HousingCatalog and C_HousingCatalog.GetCatalogEntryInfoByRecordID then
        local info = C_HousingCatalog.GetCatalogEntryInfoByRecordID(1, decorID, true)
        -- Same critical check as above — see warning about firstAcquisitionBonus
        if info and ((info.quantity or 0) > 0 or (info.numPlaced or 0) > 0 or (info.firstAcquisitionBonus or -1) == 0) then
            collectionCache[itemID] = true
            return true
        end
    end

    -- Cache negative result with current generation
    collectionCache[itemID] = -collectionGeneration
    return false
end

-- Check if decor is collected (by decorID) - used by DecorPreviewTab
-- Caches both positive and negative results; negatives invalidated by generation counter
local decorIDCache = {}
function HDG_Data.IsDecorCollected(decorID)
    if not decorID then return false end

    local cached = decorIDCache[decorID]
    if cached == true then return true end
    if cached and cached ~= true and cached == -collectionGeneration then return false end

    -- !!!! CRITICAL: DO NOT REMOVE firstAcquisitionBonus check — see IsItemCollected !!!!
    if C_HousingCatalog and C_HousingCatalog.GetCatalogEntryInfoByRecordID then
        local info = C_HousingCatalog.GetCatalogEntryInfoByRecordID(1, decorID, true)
        if info and ((info.quantity or 0) > 0 or (info.numPlaced or 0) > 0 or (info.firstAcquisitionBonus or -1) == 0) then
            decorIDCache[decorID] = true
            return true
        end
    end

    decorIDCache[decorID] = -collectionGeneration
    return false
end

-- Mark a decor as collected (called when HOUSE_DECOR_ADDED_TO_CHEST fires)
-- This bypasses the Blizzard API which may have delays in updating
function HDG_Data.MarkDecorCollected(decorID)
    if not decorID then return end
    decorIDCache[decorID] = true
    -- Increment generation to invalidate all negative cache entries
    collectionGeneration = collectionGeneration + 1

    -- Reverse lookup: find itemID from AllDecorDB (O(1) per entry, avoids old O(n) decorItem scan)
    if HDG_AllDecorDB then
        for itemID, entry in pairs(HDG_AllDecorDB) do
            if entry[1] == decorID then
                collectionCache[itemID] = true
                break
            end
        end
    end
end

-- Invalidate negative collection cache only (lightweight; keeps positives + item names)
function HDG_Data.InvalidateCollectionCache()
    collectionGeneration = collectionGeneration + 1
end

-- Clear caches
function HDG_Data.ClearCaches()
    itemNameCache = {}
    itemNameLowerCache = {}
    collectionCache = {}
    collectionGeneration = collectionGeneration + 1
    decorIDCache = {}
    itemLoadTimes = {}
end

-- Get item count in bags/bank/reagent bank (includes Warband Bank)
function HDG_Data.GetItemCount(itemID)
    if not itemID then return 0 end
    -- Include Warband Bank (Account Bank)
    return C_Item.GetItemCount(itemID, true, false, true, true) or 0
end

-- ============================================================================
-- QUALITY VARIANT TRACKING (v2.1.4)
-- Maps base itemID -> all quality variants (R1, R2, R3) for material counting
-- ============================================================================

local qualityVariantsCache = nil -- [baseItemID] = { variant1, variant2, ... }

-- Build quality variants lookup from HDG_ProfessionsDB
local function BuildQualityVariantsCache()
    if qualityVariantsCache then return end
    qualityVariantsCache = {}

    if not HDG_ProfessionsDB then return end

    for _, recipe in pairs(HDG_ProfessionsDB) do
        if recipe.slots then
            for _, slot in ipairs(recipe.slots) do
                if slot.itemID and slot.variants and #slot.variants > 0 then
                    -- Only store if we haven't seen this base item before,
                    -- or if this entry has more variants
                    if not qualityVariantsCache[slot.itemID] or
                        #slot.variants > #qualityVariantsCache[slot.itemID] then
                        qualityVariantsCache[slot.itemID] = slot.variants
                    end
                end
            end
        end
    end
end

-- Get item count including all quality variants (R1 + R2 + R3)
-- Used for checking if player has enough mats for crafting
function HDG_Data.GetItemCountWithVariants(itemID)
    if not itemID then return 0 end

    -- Start with base item count
    local total = C_Item.GetItemCount(itemID, true, false, true, true) or 0

    -- Build variants cache if needed
    BuildQualityVariantsCache()

    -- Add counts from all quality variants
    local variants = qualityVariantsCache and qualityVariantsCache[itemID]
    if variants then
        for _, variantID in ipairs(variants) do
            total = total + (C_Item.GetItemCount(variantID, true, false, true, true) or 0)
        end
    end

    return total
end

-- Get item count in personal bags/bank/reagent bank ONLY (excludes Warband Bank)
-- Used for saving per-character inventory to avoid double-counting shared Warband Bank
function HDG_Data.GetPersonalItemCount(itemID)
    if not itemID then return 0 end
    -- Exclude Warband Bank (5th param = false)
    return C_Item.GetItemCount(itemID, true, false, true, false) or 0
end

-- Get item count in Warband Bank ONLY
function HDG_Data.GetWarbandBankCount(itemID)
    if not itemID then return 0 end
    local total = C_Item.GetItemCount(itemID, true, false, true, true) or 0
    local personal = C_Item.GetItemCount(itemID, true, false, true, false) or 0
    return total - personal
end

-- ============================================================================
-- ACCOUNT-WIDE LUMBER TRACKING (v2.1.4)
-- Store and retrieve lumber counts across all characters
-- ============================================================================

-- Save current character's lumber inventory to Store
function HDG_Data.SaveCharacterLumberInventory()
    local charKey = HDG_Data.GetCharacterKey()
    if not charKey then return end

    -- Build lumber inventory (personal only - excludes Warband Bank to avoid double-counting)
    local lumberInventory = {}

    for lumberId, _ in pairs(LUMBER_TYPES) do
        -- Use personal count only (bags + bank + reagent bank, NOT Warband Bank)
        local count = HDG_Data.GetPersonalItemCount(lumberId)
        if count > 0 then
            lumberInventory[lumberId] = count
        end
    end

    -- Dispatch update to Store
    Dispatch("UPDATE_CHARACTER_DATA", {
        charKey = charKey,
        lumberInventory = lumberInventory,
        lumberUpdated = time(),
    })
end

-- Get account-wide lumber total for a specific lumber type
-- Returns: total count, table of per-character breakdowns
-- Note: Warband Bank is shared account-wide, so we add it once (not per-character)
function HDG_Data.GetAccountWideLumberCount(lumberId)
    local characters = GetCharacters()
    if not characters or not next(characters) then
        return HDG_Data.GetItemCount(lumberId), {}
    end

    local total = 0
    local breakdown = {}
    local currentChar = HDG_Data.GetCharacterKey()

    -- Sum personal inventories from all characters
    for charKey, charData in pairs(characters) do
        local count = 0

        if charKey == currentChar then
            -- Use live personal count for current character (excludes Warband Bank)
            count = HDG_Data.GetPersonalItemCount(lumberId)
        elseif charData.lumberInventory and charData.lumberInventory[lumberId] then
            -- Use saved count for other characters (already excludes Warband Bank)
            count = charData.lumberInventory[lumberId]
        end

        if count > 0 then
            total = total + count
            -- Extract just character name (without realm) for display
            local charName = charKey:match("^([^-]+)") or charKey
            table.insert(breakdown, {
                name = charName,
                fullName = charKey,
                count = count,
                isCurrent = (charKey == currentChar)
            })
        end
    end

    -- Add Warband Bank count once (it's shared account-wide)
    local warbandCount = HDG_Data.GetWarbandBankCount(lumberId)
    if warbandCount > 0 then
        total = total + warbandCount
        table.insert(breakdown, {
            name = "Warband Bank",
            fullName = "Warband Bank",
            count = warbandCount,
            isCurrent = false,
            isWarband = true
        })
    end

    -- Sort breakdown: current character first, then Warband Bank, then by count descending
    table.sort(breakdown, function(a, b)
        if a.isCurrent ~= b.isCurrent then
            return a.isCurrent
        end
        if a.isWarband ~= b.isWarband then
            return a.isWarband -- Warband Bank second after current char
        end
        return a.count > b.count
    end)

    return total, breakdown
end

-- Get per-character lumber breakdown for tooltip display
function HDG_Data.GetLumberBreakdown(lumberId)
    local _, breakdown = HDG_Data.GetAccountWideLumberCount(lumberId)
    return breakdown
end

-- ============================================================================
-- RECIPE KNOWLEDGE
-- ============================================================================

-- Check if player knows recipe by spellID (Primary method)
function HDG_Data.PlayerKnowsRecipeBySpellID(spellID)
    if not spellID or spellID == 0 then return false end
    return C_SpellBook.IsSpellKnown(spellID)
end

-- Build cache of known recipe spellIDs (Fallback)
function HDG_Data.BuildKnownRecipeCache()
    if recipeSpellsBuilt then return end

    local recipeIDs = C_TradeSkillUI.GetAllRecipeIDs()
    if not recipeIDs or #recipeIDs == 0 then return end

    for _, recipeID in ipairs(recipeIDs) do
        local recipeInfo = C_TradeSkillUI.GetRecipeInfo(recipeID)
        if recipeInfo and recipeInfo.name and recipeInfo.learned then
            knownRecipeSpells[recipeInfo.name] = recipeID
        end
    end

    recipeSpellsBuilt = true
end

-- Invalidate recipe cache
function HDG_Data.InvalidateRecipeCache()
    knownRecipeSpells = {}
    recipeSpellsBuilt = false
end

-- Check if player knows recipe by name (Legacy/Fallback)
function HDG_Data.PlayerKnowsRecipe(recipeName)
    if not recipeName then return false end
    HDG_Data.BuildKnownRecipeCache()
    return knownRecipeSpells[recipeName] ~= nil
end

-- Get spellID for a recipe name
function HDG_Data.GetRecipeSpellID(recipeName)
    if not recipeName then return nil end
    HDG_Data.BuildKnownRecipeCache()
    return knownRecipeSpells[recipeName]
end

-- Check if player has profession (uses centralized profession data)
function HDG_Data.PlayerHasProfession(professionName)
    local prof1, prof2, archaeology, fishing, cooking = GetProfessions()

    local targetID = HDG_ExpansionData.GetProfessionID(professionName)
    if not targetID then return false end

    local profIndexes = { prof1, prof2, cooking }
    for _, profIndex in ipairs(profIndexes) do
        if profIndex then
            local _, _, _, _, _, _, skillLine = GetProfessionInfo(profIndex)
            if skillLine == targetID then
                return true
            end
        end
    end

    return false
end

-- Open profession window and navigate to a specific recipe
-- professionName: "Alchemy", "Blacksmithing", etc.
-- recipeSpellID: The spell ID of the recipe to navigate to
function HDG_Data.OpenProfessionAtRecipe(professionName, recipeSpellID)
    if not recipeSpellID then return false end

    -- If profession window is already open, just navigate to recipe
    if C_TradeSkillUI and C_TradeSkillUI.IsTradeSkillReady and C_TradeSkillUI.IsTradeSkillReady() then
        if C_TradeSkillUI.OpenRecipe then
            C_TradeSkillUI.OpenRecipe(recipeSpellID)
        end
        return true
    end

    -- Need to open profession first
    if not professionName then return false end

    local targetID = HDG_ExpansionData.GetProfessionID(professionName)
    if not targetID then return false end

    -- Find the profession and cast the spell to open it
    local prof1, prof2, archaeology, fishing, cooking = GetProfessions()
    local profIndexes = { prof1, prof2, cooking }

    for _, profIndex in ipairs(profIndexes) do
        if profIndex then
            local name, icon, skillLevel, maxSkillLevel, numAbilities, spelloffset, skillLine = GetProfessionInfo(
            profIndex)
            if skillLine == targetID and spelloffset then
                -- Cast the profession spell to open the window
                CastSpell(spelloffset + 1, BOOKTYPE_PROFESSION)

                -- Wait for window to open, then navigate to recipe
                C_Timer.After(0.5, function()
                    if C_TradeSkillUI and C_TradeSkillUI.OpenRecipe then
                        C_TradeSkillUI.OpenRecipe(recipeSpellID)
                    end
                end)
                return true
            end
        end
    end

    return false
end

-- Get player professions (filtered list for UI display)
function HDG_Data.GetPlayerProfessions()
    local playerProfessions = {}

    -- Get the per-character profession filter
    local filter = HDG_Data.GetProfessionFilter()

    -- Filter professions from DB
    if privateDB.professions then
        for _, profData in ipairs(privateDB.professions) do
            -- Show all professions if filter is "All", otherwise show only selected
            if filter == "All" or profData.name == filter then
                table.insert(playerProfessions, profData)
            end
        end
    end

    return playerProfessions
end

-- ============================================================================
-- DATA PROCESSING
-- ============================================================================

-- Function to transform HDG_DecorDB into addon format
function HDG_Data.InitializeDatabase()
    if not HDG_DecorDB then
        print("|cFFFF0000[Housing Decor Guide]|r ERROR: HDG_DecorDB not found!")
        return false
    end

    -- Initialize structures in privateDB
    privateDB.professions = {}
    privateDB.decorItem = {}

    -- Use centralized expansion sort order
    local expansionOrder = HDG_ExpansionData.EXPANSION_SORT_ORDER

    -- Group recipes by profession
    local professionGroups = {}

    for dbKey, recipe in pairs(HDG_DecorDB) do
        local profName = recipe.profession
        local expansion = recipe.expansion or "Unknown"

        if not professionGroups[profName] then
            professionGroups[profName] = {}
        end

        local correctSpellID = recipe.spellID

        local extractedExpansion = expansion:match("^([^%s]+)") or "Unknown"
        local normalizedExpansion = HDG_ExpansionData.NormalizeToDisplayName(extractedExpansion)

        local recipeEntry = {
            id = recipe.itemID,
            name = recipe.name,
            spell = correctSpellID,
            decorID = recipe.decorID,
            skill = expansion,
            skillNeeded = 1,
            expansion = normalizedExpansion,
            reagents = {},
            expansionOrder = expansionOrder[extractedExpansion] or 999
        }

        if recipe.reagents then
            for reagentID, reagentData in pairs(recipe.reagents) do
                local source = (HDG_ReagentsDB and HDG_ReagentsDB[reagentID] and HDG_ReagentsDB[reagentID][1]) or "Unknown"
                table.insert(recipeEntry.reagents, {
                    id = reagentID,
                    name = reagentData.name, -- Include reagent name for search
                    amount = reagentData.qty or reagentData.amount or 1,
                    source = source
                })
            end
        end

        table.insert(professionGroups[profName], recipeEntry)

        if recipe.itemID and recipe.decorID then
            privateDB.decorItem[recipe.itemID] = {
                decorID = recipe.decorID
            }
        end
    end

    -- Convert groups to array and sort
    for profName, recipes in pairs(professionGroups) do
        table.sort(recipes, function(a, b)
            if a.expansionOrder ~= b.expansionOrder then
                return a.expansionOrder < b.expansionOrder
            end
            return (a.skill or "") < (b.skill or "")
        end)

        table.insert(privateDB.professions, {
            name = profName,
            items = recipes
        })
    end

    table.sort(privateDB.professions, function(a, b)
        return a.name < b.name
    end)

    return true
end

-- ============================================================================
-- LUMBER FUNCTIONS (delegates to HDG_LumberTracker)
-- Note: Materials functions (AggregateMaterials, FilterAndSortMaterials,
-- CategorizeMaterials) are defined directly in HDG_RecipesTab - no delegation
-- ============================================================================

-- Calculate lumber needs
function HDG_Data.CalculateLumberNeeds(professions)
    return HDG_LumberTracker.CalculateLumberNeeds(professions)
end

-- Calculate lumber needs from crafting queue
function HDG_Data.CalculateCraftingQueueLumber(queue)
    return HDG_LumberTracker.CalculateCraftingQueueLumber(queue)
end

-- ============================================================================
-- PRICE INTEGRATION (Delegated to HDG_PriceIntegration module)
-- ============================================================================

-- Delegate to centralized price integration module
function HDG_Data.GetItemPrice(itemID)
    return HDG_PriceIntegration.GetItemPrice(itemID)
end

function HDG_Data.FormatGold(copper, useTextures)
    return HDG_PriceIntegration.FormatGold(copper, useTextures)
end

function HDG_Data.HasPriceAddon()
    return HDG_PriceIntegration.HasPriceAddon()
end

-- ============================================================================
-- CRAFTING HISTORY (delegates to HDG_DataTab)
-- ============================================================================

-- Log a craft from item link (new method using TRADE_SKILL_ITEM_CRAFTED_RESULT)
function HDG_Data.LogCraftFromItemLink(itemLink, quantity)
    return HDG_DataTab.LogCraftFromItemLink(itemLink, quantity)
end

-- Get crafting history
function HDG_Data.GetCraftingHistory()
    return HDG_DataTab.GetCraftingHistory()
end

-- Clear crafting history
function HDG_Data.ClearCraftingHistory()
    return HDG_DataTab.ClearCraftingHistory()
end

-- Format timestamp for display
function HDG_Data.FormatTimestamp(timestamp)
    return HDG_DataTab.FormatTimestamp(timestamp)
end

-- ============================================================================
-- RECIPE FILTERS & SEARCH (extracted from HDG_RecipesTab Phase 1 refactor)
-- ============================================================================

function HDG_Data.GetProfessionFilter()
    local charKey = HDG_Data.GetCharacterKey()
    local charFilters = GetUIState("characterProfessionFilter")
    if charKey and charFilters and charFilters[charKey] then
        return charFilters[charKey]
    end
    return "All"
end

function HDG_Data.SetProfessionFilter(professionName)
    local charKey = HDG_Data.GetCharacterKey()
    Dispatch("SET_CHARACTER_PROFESSION_FILTER", { charKey = charKey, filter = professionName })
end

function HDG_Data.GetSearchFilter()
    return recipeSearchFilter
end

function HDG_Data.SetSearchFilter(text)
    recipeSearchFilter = (text or ""):lower()
end

-- Search reagents filter
function HDG_Data.IsSearchReagentsEnabled()
    return GetConfig("searchReagents") == true
end

function HDG_Data.SetSearchReagents(enabled)
    Dispatch("SET_CONFIG", { key = "searchReagents", value = enabled })
end

-- Uncollected only filter
function HDG_Data.IsUncollectedFilterEnabled()
    return GetConfig("uncollectedFilter") == true
end

function HDG_Data.SetUncollectedFilter(enabled)
    Dispatch("SET_CONFIG", { key = "uncollectedFilter", value = enabled })
end

-- Check if a recipe matches current filters
function HDG_Data.RecipeMatchesFilters(recipe)
    -- Search filter
    local search = recipeSearchFilter
    if search and search ~= "" then
        local recipeName = GetRecipeLowerName(recipe)
        local matchesName = recipeName:find(search, 1, true)

        -- Also search reagent names if enabled
        local matchesReagent = false
        if not matchesName and HDG_Data.IsSearchReagentsEnabled() and recipe.reagents then
            for _, reagent in ipairs(recipe.reagents) do
                -- Use reagent.name from DecorDB (faster, always available)
                local reagentName = reagent.name
                if reagentName then
                    reagentName = reagentName:lower()
                    if reagentName:find(search, 1, true) then
                        matchesReagent = true
                        break
                    end
                end
            end
        end

        if not matchesName and not matchesReagent then
            return false
        end
    end

    -- Uncollected filter
    if HDG_Data.IsUncollectedFilterEnabled() then
        local isCollected = HDG_Data.IsItemCollected(recipe.id)
        if isCollected then
            return false
        end
    end

    return true
end

-- ============================================================================
-- "WHAT CAN I CRAFT?" MODE
-- ============================================================================

-- Check if player can craft a specific recipe with current inventory
function HDG_Data.CanCraftRecipe(recipe)
    if not recipe or not recipe.reagents or #recipe.reagents == 0 then
        return false, 0, nil, 0
    end

    local maxCraftable = 999999 -- Start with a very high number
    local missingMats = {}
    local canCraft = true

    -- Track material availability for quantity-weighted percentage calculation
    -- Formula: sum(min(have, need)) / sum(need) * 100
    -- This weights each material by how much is needed, not treating each type equally
    local totalQuantityNeeded = 0
    local totalQuantityHave = 0

    for _, reagent in ipairs(recipe.reagents) do
        -- Use GetItemCountWithVariants to count all quality ranks (R1+R2+R3)
        local have = HDG_Data.GetItemCountWithVariants(reagent.id)
        local need = reagent.amount or 1

        -- Accumulate for quantity-weighted percentage (cap have at need, don't count excess)
        totalQuantityNeeded = totalQuantityNeeded + need
        totalQuantityHave = totalQuantityHave + math.min(have, need)

        if have < need then
            -- Missing this material
            canCraft = false
            table.insert(missingMats, {
                id = reagent.id,
                have = have,
                need = need,
                missing = need - have
            })
        else
            -- Fully have this material
            -- Calculate how many times we could craft based on this material
            local possibleFromThisMat = math.floor(have / need)
            if possibleFromThisMat < maxCraftable then
                maxCraftable = possibleFromThisMat
            end
        end
    end

    -- Calculate quantity-weighted material percentage
    local materialPercentage = totalQuantityNeeded > 0 and (totalQuantityHave / totalQuantityNeeded * 100) or 0

    if not canCraft then
        return false, 0, missingMats, materialPercentage
    end

    return true, maxCraftable, nil, 100
end

-- Get all craftable recipes for a profession (or all professions)
function HDG_Data.GetCraftableRecipes(professionFilter)
    local craftable = {}

    if not privateDB.professions then return craftable end

    for _, profession in ipairs(privateDB.professions) do
        -- Apply profession filter if specified
        if not professionFilter or professionFilter == "All" or profession.name == professionFilter then
            for _, recipe in ipairs(profession.items) do
                -- Only check recipes the player knows
                local knowsRecipe = HDG_Data.PlayerKnowsRecipeBySpellID(recipe.spell)

                if knowsRecipe then
                    local canCraft, maxQty = HDG_Data.CanCraftRecipe(recipe)
                    if canCraft and maxQty > 0 then
                        table.insert(craftable, {
                            recipe = recipe,
                            profession = profession.name,
                            maxQuantity = maxQty
                        })
                    end
                end
            end
        end
    end

    return craftable
end

-- Check if "What Can I Craft" mode is enabled
function HDG_Data.IsCraftableFilterEnabled()
    return GetConfig("craftableFilter") == true
end

-- Toggle "What Can I Craft" mode
function HDG_Data.SetCraftableFilter(enabled)
    Dispatch("SET_CONFIG", { key = "craftableFilter", value = enabled })
end

-- ============================================================================
-- MULTI-CHARACTER PROFESSION TRACKING (v2.0)
-- ============================================================================

-- Reference centralized profession data (ID -> Name lookup)
local PROFESSION_SKILL_LINES = HDG_ExpansionData.PROFESSION_NAMES

-- Get unique character key for current character
function HDG_Data.GetCharacterKey()
    local name = UnitName("player")
    local realm = GetRealmName()
    if name and realm then
        return name .. "-" .. realm
    end
    return nil
end

-- Get list of housing decor spellIDs for a given profession
local function GetDecorSpellIDsForProfession(professionName)
    local spellIDs = {}
    if privateDB.professions then
        for _, profData in ipairs(privateDB.professions) do
            if profData.name == professionName and profData.items then
                for _, recipe in ipairs(profData.items) do
                    if recipe.spell then
                        spellIDs[recipe.spell] = true
                    end
                end
            end
        end
    end
    return spellIDs
end

-- Collect and cache current character's profession data
-- Reference centralized expansion data
HDG_Data.EXPANSION_ORDER = HDG_ExpansionData.EXPANSION_ORDER
HDG_Data.EXPANSION_ABBR = HDG_ExpansionData.EXPANSION_ABBR

-- Profession decor thresholds are loaded from data/HDG_ProfessionThresholds.lua
-- Use HDG_GetDecorThreshold(profession, expansion) to get per-profession threshold values

-- Delegate to centralized expansion data module
local function NormalizeApiExpansionName(apiName)
    return HDG_ExpansionData.NormalizeApiName(apiName)
end

function HDG_Data.CollectCharacterProfessions()
    local charKey = HDG_Data.GetCharacterKey()
    if not charKey then return end

    -- Get existing character data from Store
    local characters = GetCharacters()
    local existingCharData = characters[charKey] or { professions = {} }
    local existingProfessions = existingCharData.professions or {}

    -- Get player's professions
    local prof1, prof2, archaeology, fishing, cooking = GetProfessions()
    local profIndexes = { prof1, prof2, cooking }

    -- Get currently open profession's skill data (C_TradeSkillUI only works when prof window is open)
    local openProfSkillLevels = {}
    local openProfSkillLine = nil -- Use skillLine ID for reliable matching

    if C_TradeSkillUI.GetChildProfessionInfos then
        local childInfos = C_TradeSkillUI.GetChildProfessionInfos()
        if childInfos and #childInfos > 0 then
            local expOrder = HDG_ExpansionData and HDG_ExpansionData.EXPANSION_ORDER or {}
            for i, childInfo in ipairs(childInfos) do
                if childInfo.skillLevel and childInfo.maxSkillLevel then
                    local normalizedName
                    if childInfo.expansionName and childInfo.expansionName ~= "" then
                        normalizedName = NormalizeApiExpansionName(childInfo.expansionName)
                    end
                    -- Fallback 1: try full professionName (e.g., "Кул-тирасская алхимия" contains "Кул-тирас")
                    if (not normalizedName or normalizedName == "Unknown") and childInfo.professionName and childInfo.professionName ~= "" then
                        normalizedName = HDG_ExpansionData.GetExpansionFromSkill(childInfo.professionName)
                    end
                    -- Fallback 2: use array index to determine expansion (index 1=Classic, 2=TBC, etc.)
                    if (not normalizedName or normalizedName == "Unknown") and expOrder[i] then
                        normalizedName = expOrder[i]
                    end
                    if normalizedName and normalizedName ~= "Unknown" then
                        openProfSkillLevels[normalizedName] = {
                            current = childInfo.skillLevel,
                            max = childInfo.maxSkillLevel,
                        }
                    end
                end
            end
            -- Get the profession's skillLine ID for reliable matching
            local baseInfo = C_TradeSkillUI.GetBaseProfessionInfo()
            if baseInfo and baseInfo.professionID then
                openProfSkillLine = baseInfo.professionID
            end
        end
    end

    -- Build new professions data
    local newProfessions = {}

    for _, profIndex in ipairs(profIndexes) do
        if profIndex then
            local _, _, _, _, _, _, skillLine = GetProfessionInfo(profIndex)
            local profName = PROFESSION_SKILL_LINES[skillLine]

            if profName then

                -- Get housing decor spellIDs for this profession
                local decorSpells = GetDecorSpellIDsForProfession(profName)

                -- Count known recipes
                local knownRecipes = {}
                local knownCount = 0
                local totalCount = 0

                for spellID, _ in pairs(decorSpells) do
                    totalCount = totalCount + 1
                    if C_SpellBook.IsSpellKnown(spellID) then
                        knownRecipes[spellID] = true
                        knownCount = knownCount + 1
                    end
                end

                -- Determine skill levels: use fresh data if this is the open profession,
                -- otherwise preserve existing cached data (with normalization)
                local skillLevels = {}
                if openProfSkillLine == skillLine and next(openProfSkillLevels) then
                    -- This profession window is open - use fresh skill data (already normalized)
                    skillLevels = openProfSkillLevels
                elseif existingProfessions[profName] and existingProfessions[profName].skillLevels then
                    -- Preserve existing cached skill levels, but normalize keys to fix legacy data
                    -- (e.g., "Outland" -> "The Burning Crusade")
                    local oldSkillLevels = existingProfessions[profName].skillLevels
                    for expName, skillData in pairs(oldSkillLevels) do
                        local normalizedName = NormalizeApiExpansionName(expName)
                        -- Only keep the highest value if we have duplicates after normalization
                        if not skillLevels[normalizedName] or skillData.current > (skillLevels[normalizedName].current or 0) then
                            skillLevels[normalizedName] = skillData
                        end
                    end
                end

                newProfessions[profName] = {
                    knownRecipes = knownRecipes,
                    knownCount = knownCount,
                    totalCount = totalCount,
                    skillLevels = skillLevels,
                }
            end
        end
    end

    -- Check if current character knows Find Lumber (spell 1256697)
    local knowsFindLumber = IsPlayerSpell(1256697) or false

    -- Dispatch update to Store
    Dispatch("UPDATE_CHARACTER_DATA", {
        charKey = charKey,
        professions = newProfessions,
        knowsFindLumber = knowsFindLumber,
        lastUpdated = time(),
    })

    -- Return the updated character data for immediate use
    return {
        professions = newProfessions,
        lastUpdated = time(),
    }
end

-- Get cached profession data for all characters (excluding current)
function HDG_Data.GetAltProfessionData(professionName)
    local currentChar = HDG_Data.GetCharacterKey()
    local altData = {}
    local characters = GetCharacters()

    for charKey, charData in pairs(characters) do
        if charKey ~= currentChar and charData.professions and charData.professions[professionName] then
            local profData = charData.professions[professionName]
            table.insert(altData, {
                character = charKey,
                knownCount = profData.knownCount or 0,
                totalCount = profData.totalCount or 0,
            })
        end
    end

    -- Sort by known count (highest first)
    table.sort(altData, function(a, b) return a.knownCount > b.knownCount end)

    return altData
end

-- Get the best alt's recipe count for a profession (highest known count)
function HDG_Data.GetBestAltRecipeCount(professionName)
    local altData = HDG_Data.GetAltProfessionData(professionName)
    if #altData > 0 then
        return altData[1].knownCount, altData[1].totalCount, altData[1].character
    end
    return 0, 0, nil
end

-- Check if any alt has a given profession
function HDG_Data.AnyAltHasProfession(professionName)
    local altData = HDG_Data.GetAltProfessionData(professionName)
    return #altData > 0
end

-- Get all characters that know a specific recipe by spellID
-- Returns array of { name = "CharName-Realm", canCraft = true/false }
-- canCraft is true if they know the recipe AND have the profession
function HDG_Data.GetCharactersWhoKnowRecipe(spellID, professionName)
    local result = {}
    local characters = GetCharacters()

    -- Check current character first
    local currentChar = HDG_Data.GetCharacterKey()
    local currentKnows = C_SpellBook.IsSpellKnown(spellID)
    if currentKnows then
        table.insert(result, {
            name = currentChar,
            isCurrent = true,
        })
    end

    -- Check all other characters
    for charKey, charData in pairs(characters) do
        if charKey ~= currentChar and charData.professions then
            -- Check if this character has the profession and knows the recipe
            if professionName and charData.professions[professionName] then
                local profData = charData.professions[professionName]
                if profData.knownRecipes and profData.knownRecipes[spellID] then
                    table.insert(result, {
                        name = charKey,
                        isCurrent = false,
                    })
                end
            else
                -- No profession specified, check all professions
                for _, profData in pairs(charData.professions) do
                    if profData.knownRecipes and profData.knownRecipes[spellID] then
                        table.insert(result, {
                            name = charKey,
                            isCurrent = false,
                        })
                        break -- Only add character once
                    end
                end
            end
        end
    end

    -- Sort: current character first, then alphabetically
    table.sort(result, function(a, b)
        if a.isCurrent ~= b.isCurrent then
            return a.isCurrent
        end
        return a.name < b.name
    end)

    return result
end

-- Export data module functions to global if needed or keep using HDG_Data table
_G.HDG_Data = HDG_Data
