local _, privateDB = ...

-- ============================================================================
-- RECIPES TAB MODULE
-- Manages all 3 panels of the Recipes tab:
--   Left: Recipe/Profession list
--   Middle: Crafting Queue
--   Right: Required Materials
-- Lumber Tracker is in separate module (HDG_LumberTracker.lua) for widget mode
-- Extracted from HDG_UI.lua in v2.0.3 for maintainability
-- ============================================================================

local CONFIG = privateDB.CONFIG
local L = HDG_L or {} -- Localization
local HDG = _G.HousingDecorGuide
local HDG_RecipesTab = {}
privateDB.HDG_RecipesTab = HDG_RecipesTab

local mainFrame = nil
local searchDebounceTimer = nil

-- Recipes-tab search box owns its text. Lives here, not in HDG_Data, because
-- it's UI-side transient state -- the EditBox writes it on OnTextChanged and
-- BuildFilterContext() reads it back when assembling the filter ctx for
-- RecipeMatchesFilters. Session-only (not persisted across reload).
local recipeSearchFilter = ""

-- Assemble the filter context for HDG_Data.RecipeMatchesFilters. Reads
-- transient search state from the module-local + persisted toggles from Store.
local function BuildFilterContext()
    return {
        search          = recipeSearchFilter,
        searchReagents  = HDG.Store:GetConfig("searchReagents") == true,
        uncollectedOnly = HDG.Store:GetConfig("uncollectedFilter") == true,
        expansion       = HDG.Store:GetConfig("expansionFilter") or "All",
        knownOnly       = HDG.Store:GetConfig("knownFilter") == true,
    }
end

-- ============================================================================
-- THEME HELPERS (delegating to ThemeEngine canonical helpers)
-- ============================================================================

local function GetScheme() return HDG.Theme.GetCurrentScheme() end
local function RegisterWidget(w, t) HDG.Theme.RegisterWidget(w, t) end
local ApplyFont = function(fs, s, ft) HDG.Theme.ApplyFont(fs, s, ft) end


-- Recipe list migrated to WowScrollBoxList + DataProvider (see mainFrame.recipeScrollBox
-- in CreateRecipePanel). The old recipeRowPool / tierHeaderPool hand-rolled pools are
-- gone; the ScrollBox owns a single pool of bare "Button" frames, and bind-time logic
-- dispatches on elementData.kind (recipe | profHeader | tierHeader | spacer) to show
-- the right widget subset per row.
local ELEMENT_HEIGHTS = {
    recipe     = 20,
    profHeader = 24,
    tierHeader = 16,
    spacer     = 6, -- default; per-element override via data.height
}

-- Per-refresh CanCraftRecipe cache (cleared at start of each refresh cycle)
local craftabilityCache = {}
local function GetCachedCraftability(recipe)
    local key = recipe.spellID or recipe.itemID
    if not craftabilityCache[key] then
        craftabilityCache[key] = { HDG_Data.CanCraftRecipe(recipe) }
    end
    return unpack(craftabilityCache[key])
end

-- Use centralized Data() accessor from UIHelpers
local function Data()
    return HDG_UIHelpers.Data()
end

-- ============================================================================
-- INITIALIZATION
-- ============================================================================

function HDG_RecipesTab:Init(frame)
    mainFrame = frame
    privateDB.HDG_ProfessionSidebar.Init(frame)
    privateDB.HDG_QueuePanel.Init(frame)
    privateDB.HDG_MaterialsPanel.Init(frame)
end


-- ============================================================================
-- RECIPE RENDERING (Left Panel)
-- ============================================================================

local function UpdateProfessionSectionHeader(profName)
    if mainFrame.profSectionHeader and mainFrame.profSectionText then
        mainFrame.profSectionText:SetText(profName)
        mainFrame.profSectionHeader:Show()
    end
end

local function HideProfessionSectionHeader()
    if mainFrame.profSectionHeader then
        mainFrame.profSectionHeader:Hide()
    end
end

-- Recipe row pool: reuse Button frames across refreshes instead of creating new ones
-- ============================================================================
-- RECIPE LIST SCROLLBOX: extent calculator, one-time initializer, per-bind
-- dispatch across recipe / profHeader / tierHeader / spacer element kinds.
-- ============================================================================

local function GetRecipeElementExtent(_, data)
    if data.kind == "spacer" then return data.height or 6 end
    return ELEMENT_HEIGHTS[data.kind] or 20
end

local function HideAllRecipeKindWidgets(frame)
    if frame.profIcon then frame.profIcon:Hide() end
    if frame.collectedIcon then frame.collectedIcon:Hide() end
    if frame.nameText then frame.nameText:Hide() end
    if frame.qtyText then frame.qtyText:Hide() end
    if frame.expText then frame.expText:Hide() end
    if frame.profHeaderLabel then frame.profHeaderLabel:Hide() end
    if frame.profCountLabel then frame.profCountLabel:Hide() end
    if frame.tierHeaderText then frame.tierHeaderText:Hide() end
end

local function InitializeRecipeListRow(frame)
    if frame._hdgInit then return end
    frame._hdgInit = true

    -- Full-frame bg texture (replaces SetBackdrop; ScrollBox-created frames are
    -- bare Button frames with no template).
    frame.bg = frame:CreateTexture(nil, "BACKGROUND")
    frame.bg:SetAllPoints()

    -- === recipe-kind widgets ===
    frame.profIcon = frame:CreateTexture(nil, "ARTWORK")
    frame.profIcon:SetSize(14, 14)
    frame.collectedIcon = frame:CreateTexture(nil, "OVERLAY")
    frame.collectedIcon:SetSize(12, 12)
    frame.nameText = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    frame.nameText:SetJustifyH("LEFT")
    frame.nameText:SetWordWrap(false)
    frame.qtyText = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    frame.expText = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    frame.expText:SetPoint("RIGHT", -5, 0)

    -- === profHeader-kind widgets (inline "All Professions" section label) ===
    frame.profCountLabel = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    frame.profCountLabel:SetPoint("RIGHT", -8, 0)
    frame.profHeaderLabel = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    frame.profHeaderLabel:SetPoint("LEFT", 8, 0)
    frame.profHeaderLabel:SetPoint("RIGHT", frame.profCountLabel, "LEFT", -8, 0)
    frame.profHeaderLabel:SetJustifyH("LEFT")
    frame.profHeaderLabel:SetWordWrap(false)

    -- === tierHeader-kind widgets ===
    frame.tierHeaderText = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    frame.tierHeaderText:SetPoint("LEFT", 8, 0)

    frame:RegisterForClicks("LeftButtonUp", "RightButtonUp")

    -- Single OnClick/OnEnter/OnLeave, dispatch on frame._kind
    frame:SetScript("OnClick", function(self)
        if self._kind == "recipe" and self._recipe then
            HDG_RecipesTab:AddToCraftingQueue(self._recipe)
        end
    end)

    frame:SetScript("OnEnter", function(self)
        if self._kind ~= "recipe" or not self._recipe then return end
        local r = self._recipe
        GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
        GameTooltip:SetItemByID(r.itemID)
        GameTooltip:AddLine(" ")
        if self._isAltKnown and self._altWhoKnows then
            GameTooltip:AddLine(HDG.Theme.C_Warning() .. (L["QUEUE_KNOWN_BY"] or "Known by:") .. "|r " .. self._altWhoKnows, 1, 1, 1)
            GameTooltip:AddLine(" ")
        end
        if r.reagents and #r.reagents > 0 then
            GameTooltip:AddLine(HDG.Theme.C_Text() .. L["HEADER_MATERIALS"] .. ":|r")
            for _, reagent in ipairs(r.reagents) do
                local have = HDG_Data.GetItemCountWithVariants(reagent.id)
                local need = reagent.qty or 1
                local reagentName = reagent.name or Data().GetCachedItemName(reagent.id) or ("Item " .. reagent.id)
                local color = have >= need and HDG.Theme.C_Success() or (have > 0 and HDG.Theme.C_Warning() or HDG.Theme.C_Error())
                GameTooltip:AddDoubleLine("  " .. reagentName, string.format("%s%d/%d|r", color, have, need), 1, 1, 1, 1, 1, 1)
            end
        end
        GameTooltip:AddLine(" ")
        GameTooltip:AddLine(L["RECIPE_TIP_CLICK_ADD_QUEUE"], 0.7, 0.7, 0.7)
        GameTooltip:Show()
        HDG_UIHelpers.ShowHoverPreview(r.itemID, mainFrame)
    end)

    frame:SetScript("OnLeave", function(self)
        if self._kind == "recipe" then
            HDG_UIHelpers.HideHoverPreview()
        end
        GameTooltip:Hide()
    end)
end

-- Per-bind: update a frame to display the given recipe element
local function BindRecipeKind(frame, data)
    local recipe = data.recipe
    frame._kind = "recipe"
    frame._recipe = recipe
    frame._isAltKnown = data.isAltKnown
    frame._altWhoKnows = data.altWhoKnows

    local itemName = Data().GetLocalizedName(recipe.itemID, recipe.name)
    if not itemName or itemName == "Loading..." then itemName = "Item " .. recipe.itemID end

    local isCollected = Data().IsItemCollected(recipe.itemID)

    local expColor = HDG_ExpansionData.GetColor(recipe.expansion)
    if expColor then
        frame.bg:SetColorTexture(expColor[1], expColor[2], expColor[3], 0.25)
    else
        local scheme = GetScheme()
        frame.bg:SetColorTexture(scheme.button_normal.r, scheme.button_normal.g, scheme.button_normal.b, 0.2)
    end

    local leftOffset = 3
    if data.showProfessionIcon and recipe._professionName then
        frame.profIcon:ClearAllPoints()
        frame.profIcon:SetPoint("LEFT", leftOffset, 0)
        HDG_ExpansionData.ApplyProfessionIcon(frame.profIcon, recipe._professionName)
        frame.profIcon:Show()
        leftOffset = leftOffset + 16
    end

    frame.collectedIcon:ClearAllPoints()
    frame.collectedIcon:SetPoint("LEFT", leftOffset, 0)
    frame.collectedIcon:SetTexture(isCollected and CONFIG.TEXTURES.CHECKMARK or CONFIG.TEXTURES.XMARK)
    frame.collectedIcon:Show()
    leftOffset = leftOffset + 15

    local btnScheme = GetScheme()
    frame.nameText:ClearAllPoints()
    frame.nameText:SetPoint("LEFT", leftOffset, 0)
    frame.nameText:SetPoint("RIGHT", -55, 0)
    ApplyFont(frame.nameText, btnScheme, "small")

    local recipeColor
    if data.noProfession then
        recipeColor = btnScheme.text_recipe_no_prof or {r=0.56, g=0.56, b=0.56, a=1}
    elseif data.isKnown then
        recipeColor = btnScheme.text_recipe_known or {r=0.80, g=0.29, b=0.09, a=1}
    elseif data.isAltKnown then
        recipeColor = btnScheme.text_recipe_alt or {r=1, g=1, b=1, a=1}
    else
        recipeColor = btnScheme.text_recipe_unknown or {r=0.69, g=0.69, b=0.69, a=1}
    end
    frame.nameText:SetText(itemName)
    frame.nameText:SetTextColor(recipeColor.r, recipeColor.g, recipeColor.b, recipeColor.a)
    frame.nameText:Show()

    if data.showCraftableQty then
        frame.qtyText:ClearAllPoints()
        frame.qtyText:SetPoint("RIGHT", -40, 0)
        ApplyFont(frame.qtyText, btnScheme, "small")
        if recipe._maxCraftable and recipe._maxCraftable > 0 then
            local maxQty = recipe._maxCraftable
            if maxQty > 99 then
                frame.qtyText:SetText(HDG.Theme.C_Success() .. "x99+|r")
            else
                frame.qtyText:SetText(string.format(HDG.Theme.C_Success() .. "x%d|r", maxQty))
            end
            frame.qtyText:Show()
        elseif recipe._materialPercent then
            local pct = recipe._materialPercent
            local color = pct >= 75 and HDG.Theme.C_Success() or (pct >= 50 and HDG.Theme.C_Warning() or HDG.Theme.C_Error())
            frame.qtyText:SetText(string.format("%s%.0f%%|r", color, pct))
            frame.qtyText:Show()
        else
            frame.qtyText:Hide()
        end
    else
        frame.qtyText:Hide()
    end

    local expShort = HDG_ExpansionData.GetShortName(recipe.expansion)
    local expColor2 = HDG_ExpansionData.GetColor(recipe.expansion)
    frame.expText:SetText(expShort)
    frame.expText:SetTextColor(unpack(expColor2))
    ApplyFont(frame.expText, btnScheme, "small")
    frame.expText:Show()
end

local function BindProfHeaderKind(frame, data)
    frame._kind = "profHeader"
    frame._recipe = nil
    local scheme = GetScheme()
    local headerBg = scheme.header_bar or scheme.panel or {r=0.08, g=0.25, b=0.25, a=0.9}
    frame.bg:SetColorTexture(headerBg.r, headerBg.g, headerBg.b, headerBg.a or 0.9)

    ApplyFont(frame.profCountLabel, scheme, "small")
    frame.profCountLabel:SetText(HDG.Theme.C_Dim() .. (data.count or 0) .. " recipes|r")
    frame.profCountLabel:Show()

    ApplyFont(frame.profHeaderLabel, scheme, "body")
    frame.profHeaderLabel:SetText(data.text or "")
    local profColor = data.hasProfession
        and (scheme.text_recipe_known or {r=0.80, g=0.29, b=0.09, a=1})
        or (scheme.text_recipe_no_prof or {r=0.56, g=0.56, b=0.56, a=1})
    frame.profHeaderLabel:SetTextColor(profColor.r, profColor.g, profColor.b, profColor.a or 1)
    frame.profHeaderLabel:Show()
end

local function BindTierHeaderKind(frame, data)
    frame._kind = "tierHeader"
    frame._recipe = nil
    frame.bg:SetColorTexture(0, 0, 0, 0)
    ApplyFont(frame.tierHeaderText, GetScheme(), "small")
    frame.tierHeaderText:SetText((data.color or "") .. (data.label or "") .. "|r " .. HDG.Theme.C_Dim() .. "(" .. (data.count or 0) .. ")|r")
    frame.tierHeaderText:Show()
end

local function BindSpacerKind(frame)
    frame._kind = "spacer"
    frame._recipe = nil
    frame.bg:SetColorTexture(0, 0, 0, 0)
end

local function BindRecipeListRow(frame, data)
    HideAllRecipeKindWidgets(frame)
    local kind = data.kind
    if kind == "recipe" then
        BindRecipeKind(frame, data)
    elseif kind == "profHeader" then
        BindProfHeaderKind(frame, data)
    elseif kind == "tierHeader" then
        BindTierHeaderKind(frame, data)
    else
        BindSpacerKind(frame)
    end
end

-- ============================================================================
-- DATAPROVIDER PUSH HELPERS
-- Replace the old yOffset-accumulating Render* functions. Each Push* function
-- appends zero or more elements to the DataProvider.
-- ============================================================================

-- Precompute recipe render state so bind has no I/O except the tooltip OnEnter.
local function PushRecipe(dp, recipe, showCraftableQty, noProfession, showProfessionIcon)
    local isKnown = Data().PlayerKnowsRecipeBySpellID and Data().PlayerKnowsRecipeBySpellID(recipe.spellID) or false
    local isAltKnown = false
    local altWhoKnows = nil
    if not isKnown and recipe.spellID and HDG_Data.GetCharactersWhoKnowRecipe then
        local altsWhoKnow = HDG_Data.GetCharactersWhoKnowRecipe(recipe.spellID, recipe.profession)
        if altsWhoKnow then
            for _, altInfo in ipairs(altsWhoKnow) do
                if not altInfo.isCurrent then
                    isAltKnown = true
                    altWhoKnows = altInfo.name
                    break
                end
            end
        end
    end

    dp:Insert({
        kind = "recipe",
        recipe = recipe,
        showCraftableQty = showCraftableQty,
        noProfession = noProfession,
        showProfessionIcon = showProfessionIcon,
        isKnown = isKnown,
        isAltKnown = isAltKnown,
        altWhoKnows = altWhoKnows,
    })
end

local function sortByPercent(a, b)
    return (a._materialPercent or 0) > (b._materialPercent or 0)
end

local function PushTierGroup(dp, tier, label, color, showProfessionIcon)
    if #tier == 0 then return end
    dp:Insert({ kind = "tierHeader", label = label, color = color, count = #tier })
    for _, recipe in ipairs(tier) do
        PushRecipe(dp, recipe, true, recipe._noProfession, showProfessionIcon)
    end
    dp:Insert({ kind = "spacer", height = 6 })
end

local function PushProfession(dp, profData, showInlineHeader, skipFixedHeader)
    local internalProfName = profData.name                                          -- English name for lookups
    local profName = HDG_ExpansionData.GetLocalizedProfessionName(internalProfName) -- Localized for display
    local craftableFilterEnabled = HDG.Store:GetConfig("craftableFilter") == true
    local filterCtx = BuildFilterContext()

    local preFilteredItems = {}
    for _, recipe in ipairs(profData.items) do
        if HDG_Data.RecipeMatchesFilters(recipe, filterCtx) then
            table.insert(preFilteredItems, recipe)
        end
    end
    if #preFilteredItems == 0 then return end

    local hasProfession = HDG_Data.PlayerHasProfession(internalProfName)

    local function pushInlineHeader(headerText)
        if showInlineHeader then
            dp:Insert({
                kind = "profHeader",
                text = headerText,
                count = #preFilteredItems,
                hasProfession = hasProfession,
            })
        end
    end

    if craftableFilterEnabled then
        local tier100, tier75to99, tier50to74, tierBelow50 = {}, {}, {}, {}
        for _, recipe in ipairs(preFilteredItems) do
            local isKnown = HDG_Data.PlayerKnowsRecipeBySpellID(recipe.spellID)
            local canCraft, maxQty, _, matPercent = GetCachedCraftability(recipe)
            recipe._maxCraftable = maxQty
            recipe._materialPercent = matPercent
            recipe._noProfession = not hasProfession
            if hasProfession and isKnown and canCraft and maxQty > 0 then
                table.insert(tier100, recipe)
            elseif matPercent >= 75 then
                table.insert(tier75to99, recipe)
            elseif matPercent >= 50 then
                table.insert(tier50to74, recipe)
            elseif matPercent > 0 then
                table.insert(tierBelow50, recipe)
            end
        end
        table.sort(tier75to99, sortByPercent)
        table.sort(tier50to74, sortByPercent)
        table.sort(tierBelow50, sortByPercent)

        if #tier100 + #tier75to99 + #tier50to74 + #tierBelow50 == 0 then return end

        local notLearnedLabel = L["QUEUE_NEEDS_TRAINING"] or "Not Learned"
        local headerText = profName
        if not hasProfession then
            headerText = profName .. " (" .. notLearnedLabel .. ")"
        end
        pushInlineHeader(headerText)

        if not skipFixedHeader then
            local fixedHeaderText = profName .. " (" .. (L["HEADER_MATERIALS"] or "Materials") .. ")"
            if not hasProfession then
                fixedHeaderText = profName .. " " .. HDG.Theme.C_Dim() .. "(" .. notLearnedLabel .. ")|r"
            end
            UpdateProfessionSectionHeader(fixedHeaderText)
        end

        PushTierGroup(dp, tier100, L["RECIPE_TIER_READY"], HDG.Theme.C_Success())
        PushTierGroup(dp, tier75to99, L["RECIPE_TIER_ALMOST"], HDG.Theme.C_Success())
        PushTierGroup(dp, tier50to74, L["RECIPE_TIER_HALFWAY"], HDG.Theme.C_Warning())
        PushTierGroup(dp, tierBelow50, L["RECIPE_TIER_NEED_MATS"], HDG.Theme.C_Error())

        dp:Insert({ kind = "spacer", height = CONFIG.SPACING.PROFESSION_GAP or 8 })
        return
    end

    pushInlineHeader(profName)
    if not skipFixedHeader then
        UpdateProfessionSectionHeader(profName)
    end
    for _, recipe in ipairs(preFilteredItems) do
        PushRecipe(dp, recipe, false)
    end
    dp:Insert({ kind = "spacer", height = CONFIG.SPACING.PROFESSION_GAP or 8 })
end

-- ============================================================================
-- CREATE RECIPE PANEL (Left)
-- ============================================================================

-- CreateScrollPane helper retired: all three Recipes tab panels (recipe list,
-- crafting queue, materials list) now construct their own WowScrollBoxList
-- inline. No other file referenced this helper.

function HDG_RecipesTab:CreateRecipePanel()
    local scheme = GetScheme()

    -- Use factory for panel backdrop (reposition for 3-column layout)
    local layout = HDG.UI:TabPanel(mainFrame, {
        hasHeaderBar = false,
        hasScroll = false,
    })

    local recipeFrame = layout.frame
    recipeFrame:ClearAllPoints()
    recipeFrame:SetPoint("TOPLEFT", CONFIG.SPACING.PANEL_PADDING, -(CONFIG.BANNER_HEIGHT + CONFIG.TAB_HEIGHT + 8))
    recipeFrame:SetSize(CONFIG.LEFT_PANEL_WIDTH, CONFIG.PANEL_HEIGHT)
    mainFrame.recipeFrame = recipeFrame

    -- Header
    local recipeHeaderBg, recipeTitle = HDG_UIHelpers.CreatePanelHeader(recipeFrame, L["RECIPE_PROFESSIONS"] or "Professions")
    mainFrame.recipeHeaderBg = recipeHeaderBg
    mainFrame.recipeTitle = recipeTitle

    -- Column header icons with tooltips for totals
    local COL_WIDTH = 60
    local HEADER_OFFSET = 7
    local COLLECTION_COL_LEFT = -6 - COL_WIDTH + HEADER_OFFSET
    local RECIPES_COL_LEFT = -68 - COL_WIDTH + HEADER_OFFSET

    -- Recipes column header (book icon + percentage)
    local recipesHeaderFrame = CreateFrame("Frame", nil, recipeHeaderBg)
    recipesHeaderFrame:SetSize(50, 20)
    recipesHeaderFrame:SetPoint("RIGHT", RECIPES_COL_LEFT + 50, 0)
    recipesHeaderFrame:EnableMouse(true)
    mainFrame.recipesHeaderFrame = recipesHeaderFrame

    local headerRecipesIcon = recipesHeaderFrame:CreateTexture(nil, "OVERLAY")
    headerRecipesIcon:SetSize(28, 28)
    headerRecipesIcon:SetPoint("LEFT", -4, 1)
    headerRecipesIcon:SetAtlas("UI-HUD-MicroMenu-SpellbookAbilities-Up")

    local headerRecipesPct = recipesHeaderFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    headerRecipesPct:SetPoint("LEFT", headerRecipesIcon, "RIGHT", 2, 0)
    ApplyFont(headerRecipesPct, scheme, "small")
    mainFrame.headerRecipesPct = headerRecipesPct

    -- Collection column header (crate icon + percentage)
    local collectionHeaderFrame = CreateFrame("Frame", nil, recipeHeaderBg)
    collectionHeaderFrame:SetSize(50, 20)
    collectionHeaderFrame:SetPoint("RIGHT", COLLECTION_COL_LEFT + 50, 0)
    collectionHeaderFrame:EnableMouse(true)
    mainFrame.collectionHeaderFrame = collectionHeaderFrame

    local headerCollectionIcon = collectionHeaderFrame:CreateTexture(nil, "OVERLAY")
    headerCollectionIcon:SetSize(28, 28)
    headerCollectionIcon:SetPoint("LEFT", -4, 0)
    headerCollectionIcon:SetAtlas("GarrMission_CurrencyIcon-Material")

    local headerCollectionPct = collectionHeaderFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    headerCollectionPct:SetPoint("LEFT", headerCollectionIcon, "RIGHT", 2, 0)
    ApplyFont(headerCollectionPct, scheme, "small")
    mainFrame.headerCollectionPct = headerCollectionPct

    -- Search box row (right below header)
    local searchRow = CreateFrame("Frame", nil, recipeFrame)
    searchRow:SetPoint("TOPLEFT", 8, -24)
    searchRow:SetPoint("TOPRIGHT", -8, -24)
    searchRow:SetHeight(20)

    -- Profession tabs container (pushed down by search row)
    local professionTabs = CreateFrame("Frame", nil, recipeFrame)
    professionTabs:SetPoint("TOPLEFT", 8, -46)
    professionTabs:SetPoint("TOPRIGHT", -8, -46)
    professionTabs:SetHeight(188)
    mainFrame.professionTabs = professionTabs

    -- Search box
    local searchBox = CreateFrame("EditBox", nil, searchRow, "BackdropTemplate")
    searchBox:SetPoint("LEFT", 0, 0)
    searchBox:SetSize(100, 20)
    HDG_UIHelpers.ApplyBackdrop(searchBox, "panel",
        {scheme.bg.r, scheme.bg.g, scheme.bg.b, 1},
        {scheme.border.r, scheme.border.g, scheme.border.b, 0.8})
    searchBox:SetAutoFocus(false)
    searchBox:SetTextInsets(6, 20, 0, 0)
    searchBox:SetMaxLetters(50)
    ApplyFont(searchBox, scheme, "small")

    -- Search placeholder text
    local searchPlaceholder = searchBox:CreateFontString(nil, "ARTWORK")
    searchPlaceholder:SetPoint("LEFT", 6, 0)
    ApplyFont(searchPlaceholder, scheme, "small")
    searchPlaceholder:SetText(L["RECIPE_SEARCH"])
    if scheme then
        searchPlaceholder:SetTextColor(scheme.text_dim.r, scheme.text_dim.g, scheme.text_dim.b, 0.6)
    end
    searchBox.placeholder = searchPlaceholder

    -- Clear button (X) for search
    local searchClear = CreateFrame("Button", nil, searchBox)
    searchClear:SetSize(14, 14)
    searchClear:SetPoint("RIGHT", -3, 0)
    searchClear:Hide()
    local searchClearTex = searchClear:CreateTexture(nil, "ARTWORK")
    searchClearTex:SetAllPoints()
    searchClearTex:SetTexture("Interface\\Buttons\\UI-StopButton")
    searchClear:SetScript("OnClick", function()
        searchBox:SetText("")
        searchBox:ClearFocus()
        recipeSearchFilter = ""
        HDG_UI.RefreshUI()
    end)

    searchBox:SetScript("OnTextChanged", function(self, userInput)
        local text = self:GetText()
        if text and text ~= "" then
            searchPlaceholder:Hide()
            searchClear:Show()
        else
            searchPlaceholder:Show()
            searchClear:Hide()
        end
        if userInput then
            recipeSearchFilter = (text or ""):lower()
            if searchDebounceTimer then searchDebounceTimer:Cancel() end
            searchDebounceTimer = C_Timer.NewTimer(0.3, function()
                searchDebounceTimer = nil
                HDG_UI.RefreshUI()
            end)
        end
    end)
    searchBox:SetScript("OnEscapePressed", function(self)
        self:ClearFocus()
    end)
    searchBox:SetScript("OnEnterPressed", function(self)
        self:ClearFocus()
    end)
    mainFrame.recipeSearchBox = searchBox
    RegisterWidget(searchBox, "SearchBox")

    -- Uncollected checkbox
    local uncollectedCheck = CreateFrame("CheckButton", nil, searchRow, "UICheckButtonTemplate")
    uncollectedCheck:SetPoint("LEFT", searchBox, "RIGHT", 4, 0)
    uncollectedCheck:SetSize(20, 20)
    uncollectedCheck:SetChecked(HDG.Store:GetConfig("uncollectedFilter") == true)

    local uncollectedLabel = searchRow:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    uncollectedLabel:SetPoint("LEFT", uncollectedCheck, "RIGHT", 0, 0)
    uncollectedLabel:SetText(HDG.Theme.C_Text() .. "Uncollected" .. "|r") -- English only (layout policy)
    ApplyFont(uncollectedLabel, scheme, "small")

    uncollectedCheck:SetScript("OnClick", function(self)
        HDG.Store:Dispatch("SET_CONFIG", { key = "uncollectedFilter", value = self:GetChecked() })
        HDG_UI.RefreshUI()
    end)
    HDG_UIHelpers.SetupTooltip(uncollectedCheck, {
        title = HDG.Theme.C_Text() .. L["RECIPE_TIP_UNCOLLECTED_TITLE"] .. "|r",
        desc  = L["RECIPE_TIP_UNCOLLECTED_DESC"],
    })
    mainFrame.uncollectedCheck = uncollectedCheck

    -- Search reagents checkbox
    local searchReagentsCheck = CreateFrame("CheckButton", nil, searchRow, "UICheckButtonTemplate")
    searchReagentsCheck:SetPoint("LEFT", uncollectedLabel, "RIGHT", 4, 0)
    searchReagentsCheck:SetSize(20, 20)
    searchReagentsCheck:SetChecked(HDG.Store:GetConfig("searchReagents") == true)

    local searchReagentsLabel = searchRow:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    searchReagentsLabel:SetPoint("LEFT", searchReagentsCheck, "RIGHT", 0, 0)
    searchReagentsLabel:SetText(HDG.Theme.C_Text() .. (L["CHECK_INCLUDE_MATS"] or "+Mats") .. "|r")
    ApplyFont(searchReagentsLabel, scheme, "small")

    searchReagentsCheck:SetScript("OnClick", function(self)
        HDG.Store:Dispatch("SET_CONFIG", { key = "searchReagents", value = self:GetChecked() })
        HDG_UI.RefreshUI()
    end)
    HDG_UIHelpers.SetupTooltip(searchReagentsCheck, {
        title = HDG.Theme.C_Text() .. L["RECIPE_TIP_SEARCH_MATS"] .. "|r",
        desc  = { L["RECIPE_TIP_SEARCH_MATS_DESC"], L["RECIPE_TIP_SEARCH_MATS_EX"] },
    })
    mainFrame.searchReagentsCheck = searchReagentsCheck

    -- Sticky header (offset by search row height)
    local stickyHeader = CreateFrame("Frame", nil, recipeFrame, "BackdropTemplate")
    stickyHeader:SetPoint("TOPLEFT", 8, -258)
    stickyHeader:SetPoint("TOPRIGHT", -26, -258)
    stickyHeader:SetHeight(18)
    HDG_UIHelpers.ApplyBackdrop(stickyHeader, "solid", {scheme.panel.r, scheme.panel.g, scheme.panel.b, 1.0})
    RegisterWidget(stickyHeader, "Panel")
    stickyHeader:Hide()
    mainFrame.stickyHeader = stickyHeader

    local stickyHeaderText = stickyHeader:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    stickyHeaderText:SetPoint("LEFT", 6, 0)
    ApplyFont(stickyHeaderText, scheme, "small")
    stickyHeaderText:SetTextColor(scheme.text.r, scheme.text.g, scheme.text.b, scheme.text.a)
    mainFrame.stickyHeaderText = stickyHeaderText
    RegisterWidget(stickyHeaderText, "Text")

    local stickyHeaderPercentage = stickyHeader:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    stickyHeaderPercentage:SetPoint("RIGHT", -6, 0)
    ApplyFont(stickyHeaderPercentage, scheme, "small")
    stickyHeaderPercentage:SetTextColor(scheme.text.r, scheme.text.g, scheme.text.b, scheme.text.a)
    mainFrame.stickyHeaderPercentage = stickyHeaderPercentage
    RegisterWidget(stickyHeaderPercentage, "Text")

    -- Profession section header (offset by search row height)
    local profSectionHeader = CreateFrame("Frame", nil, recipeFrame, "BackdropTemplate")
    profSectionHeader:SetPoint("TOPLEFT", 1, -284)
    profSectionHeader:SetPoint("TOPRIGHT", -1, -284)
    profSectionHeader:SetHeight(22)
    HDG_UIHelpers.ApplyBackdrop(profSectionHeader, "panel",
        {scheme.bg.r, scheme.bg.g, scheme.bg.b, 0.8},
        {scheme.border.r, scheme.border.g, scheme.border.b, scheme.border.a})
    profSectionHeader:Hide()
    mainFrame.profSectionHeader = profSectionHeader
    RegisterWidget(profSectionHeader, "Panel")

    local profSectionTextFrame = CreateFrame("Frame", nil, profSectionHeader)
    profSectionTextFrame:SetPoint("LEFT", 8, 0)
    profSectionTextFrame:SetSize(150, 20)

    local profSectionText = profSectionTextFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    profSectionText:SetPoint("LEFT", 0, 0)
    ApplyFont(profSectionText, scheme, "body")
    profSectionText:SetTextColor(scheme.text.r, scheme.text.g, scheme.text.b, scheme.text.a)
    mainFrame.profSectionText = profSectionText
    RegisterWidget(profSectionText, "Text")

    HDG_UIHelpers.SetupTooltip(profSectionTextFrame, {
        title = HDG.Theme.C_Text() .. L["RECIPE_TIP_PROF_PROGRESS"] .. "|r",
        desc  = {
            L["RECIPE_TIP_PROF_PROGRESS_DESC"],
            " ",
            HDG.Theme.C_Warning() .. L["RECIPE_TIP_WORKFLOW"] .. "|r",
            L["RECIPE_TIP_WORKFLOW_1"],
            L["RECIPE_TIP_WORKFLOW_2"],
            L["RECIPE_TIP_WORKFLOW_3"],
        },
    })

    -- All / Known / Ready toggle (tri-state exclusive; "all" = no filter)
    local btnWidth = 46
    local toggleContainer = CreateFrame("Frame", nil, profSectionHeader)
    toggleContainer:SetPoint("TOPRIGHT", 0, 0)
    toggleContainer:SetPoint("BOTTOMRIGHT", 0, 0)
    toggleContainer:SetWidth(btnWidth * 3)

    local allBtn = CreateFrame("Button", nil, toggleContainer, "BackdropTemplate")
    allBtn:SetPoint("TOPLEFT", 0, 0)
    allBtn:SetPoint("BOTTOMLEFT", 0, 0)
    allBtn:SetWidth(btnWidth)
    allBtn:SetBackdrop({ bgFile = CONFIG.TEXTURES.WHITE_BG, tile = false })

    local allBtnText = allBtn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    allBtnText:SetPoint("CENTER", 0, 0)
    ApplyFont(allBtnText, scheme, "small")
    allBtnText:SetText(L["BTN_ALL"] or "All")

    local knownBtn = CreateFrame("Button", nil, toggleContainer, "BackdropTemplate")
    knownBtn:SetPoint("TOPLEFT", allBtn, "TOPRIGHT", 0, 0)
    knownBtn:SetPoint("BOTTOMLEFT", allBtn, "BOTTOMRIGHT", 0, 0)
    knownBtn:SetWidth(btnWidth)
    knownBtn:SetBackdrop({ bgFile = CONFIG.TEXTURES.WHITE_BG, tile = false })

    local knownBtnText = knownBtn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    knownBtnText:SetPoint("CENTER", 0, 0)
    ApplyFont(knownBtnText, scheme, "small")
    knownBtnText:SetText(L["BTN_KNOWN"] or "Known")

    local knownBtnLeftBorder = knownBtn:CreateTexture(nil, "OVERLAY")
    knownBtnLeftBorder:SetTexture(CONFIG.TEXTURES.WHITE_BG)
    knownBtnLeftBorder:SetVertexColor(unpack(HDG_UIHelpers.BORDER_COLOR))
    knownBtnLeftBorder:SetPoint("TOPLEFT", 0, 0)
    knownBtnLeftBorder:SetPoint("BOTTOMLEFT", 0, 0)
    knownBtnLeftBorder:SetWidth(1)
    knownBtnLeftBorder:Hide()

    local readyBtn = CreateFrame("Button", nil, toggleContainer, "BackdropTemplate")
    readyBtn:SetPoint("TOPRIGHT", 0, 0)
    readyBtn:SetPoint("BOTTOMRIGHT", 0, 0)
    readyBtn:SetWidth(btnWidth)
    readyBtn:SetBackdrop({ bgFile = CONFIG.TEXTURES.WHITE_BG, tile = false })

    local readyBtnText = readyBtn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    readyBtnText:SetPoint("CENTER", 0, 0)
    ApplyFont(readyBtnText, scheme, "small")
    readyBtnText:SetText(L["BTN_READY"] or "Ready")

    local readyBtnLeftBorder = readyBtn:CreateTexture(nil, "OVERLAY")
    readyBtnLeftBorder:SetTexture(CONFIG.TEXTURES.WHITE_BG)
    readyBtnLeftBorder:SetVertexColor(unpack(HDG_UIHelpers.BORDER_COLOR))
    readyBtnLeftBorder:SetPoint("TOPLEFT", 0, 0)
    readyBtnLeftBorder:SetPoint("BOTTOMLEFT", 0, 0)
    readyBtnLeftBorder:SetWidth(1)
    readyBtnLeftBorder:Hide()

    local function CurrentToggleMode()
        if HDG.Store:GetConfig("craftableFilter") == true then return "ready" end
        if HDG.Store:GetConfig("knownFilter") == true then return "known" end
        return "all"
    end

    local function UpdateCraftableToggle()
        local s = GetScheme()
        local mode = CurrentToggleMode()
        local function applyState(btn, text, isActive)
            btn:SetBackdropColor(
                isActive and s.button_active.r or 0.05,
                isActive and s.button_active.g or 0.05,
                isActive and s.button_active.b or 0.05,
                isActive and 0.9 or 0
            )
            text:SetTextColor(
                isActive and s.text.r or s.text_dim.r,
                isActive and s.text.g or s.text_dim.g,
                isActive and s.text.b or s.text_dim.b,
                isActive and s.text.a or s.text_dim.a
            )
        end
        applyState(allBtn, allBtnText, mode == "all")
        applyState(knownBtn, knownBtnText, mode == "known")
        applyState(readyBtn, readyBtnText, mode == "ready")
        -- Dividers visible only between inactive buttons (match prior "show border when neighbor active" behavior)
        knownBtnLeftBorder:SetShown(mode ~= "all" and mode ~= "known")
        readyBtnLeftBorder:SetShown(mode ~= "known" and mode ~= "ready")
    end

    allBtn:SetScript("OnClick", function()
        if CurrentToggleMode() ~= "all" then
            HDG.Store:Dispatch("SET_CONFIG", { key = "craftableFilter", value = false })
            HDG.Store:Dispatch("SET_CONFIG", { key = "knownFilter", value = false })
            UpdateCraftableToggle()
            HDG_UI.RefreshUI()
        end
    end)

    knownBtn:SetScript("OnClick", function()
        if CurrentToggleMode() ~= "known" then
            HDG.Store:Dispatch("SET_CONFIG", { key = "craftableFilter", value = false })
            HDG.Store:Dispatch("SET_CONFIG", { key = "knownFilter", value = true })
            UpdateCraftableToggle()
            HDG_UI.RefreshUI()
        end
    end)

    readyBtn:SetScript("OnClick", function()
        if CurrentToggleMode() ~= "ready" then
            HDG.Store:Dispatch("SET_CONFIG", { key = "knownFilter", value = false })
            HDG.Store:Dispatch("SET_CONFIG", { key = "craftableFilter", value = true })
            UpdateCraftableToggle()
            HDG_UI.RefreshUI()
        end
    end)

    allBtn:SetScript("OnEnter", function(self)
        if CurrentToggleMode() ~= "all" then
            local s = GetScheme()
            self:SetBackdropColor(s.button_hover.r, s.button_hover.g, s.button_hover.b, 0.9)
        end
        GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
        GameTooltip:AddLine(HDG.Theme.C_Text() .. L["RECIPE_TIP_SHOW_ALL"] .. "|r", 1, 1, 1)
        GameTooltip:Show()
    end)
    allBtn:SetScript("OnLeave", function() UpdateCraftableToggle(); GameTooltip:Hide() end)

    knownBtn:SetScript("OnEnter", function(self)
        if CurrentToggleMode() ~= "known" then
            local s = GetScheme()
            self:SetBackdropColor(s.button_hover.r, s.button_hover.g, s.button_hover.b, 0.9)
        end
        GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
        GameTooltip:AddLine(HDG.Theme.C_Text() .. L["RECIPE_TIP_SHOW_KNOWN"] .. "|r", 1, 1, 1)
        GameTooltip:AddLine(L["RECIPE_TIP_FILTER_KNOWN"], 1, 1, 1)
        GameTooltip:Show()
    end)
    knownBtn:SetScript("OnLeave", function() UpdateCraftableToggle(); GameTooltip:Hide() end)

    readyBtn:SetScript("OnEnter", function(self)
        if CurrentToggleMode() ~= "ready" then
            local s = GetScheme()
            self:SetBackdropColor(s.button_hover.r, s.button_hover.g, s.button_hover.b, 0.9)
        end
        GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
        GameTooltip:AddLine(HDG.Theme.C_Text() .. L["RECIPE_TIP_SHOW_READY"] .. "|r", 1, 1, 1)
        GameTooltip:AddLine(L["RECIPE_TIP_FILTER_MATS"], 1, 1, 1)
        GameTooltip:Show()
    end)
    readyBtn:SetScript("OnLeave", function() UpdateCraftableToggle(); GameTooltip:Hide() end)

    mainFrame.UpdateCraftableButton = UpdateCraftableToggle
    UpdateCraftableToggle()

    -- Scroll frame
    -- Recipe list scroll: modern WowScrollBoxList + DataProvider. Queue + materials
    -- panels still use CreateScrollPane (one migration at a time).
    local scrollBox = CreateFrame("Frame", nil, recipeFrame, "WowScrollBoxList")
    scrollBox:SetPoint("TOPLEFT", 8, -310)
    scrollBox:SetPoint("BOTTOMRIGHT", -20, 8)

    local scrollBar = CreateFrame("EventFrame", nil, recipeFrame, "MinimalScrollBar")
    scrollBar:SetPoint("TOPLEFT", scrollBox, "TOPRIGHT", 2, 0)
    scrollBar:SetPoint("BOTTOMLEFT", scrollBox, "BOTTOMRIGHT", 2, 0)

    local view = CreateScrollBoxListLinearView(0, 0, 0, 0, 0)
    view:SetElementExtentCalculator(GetRecipeElementExtent)
    view:SetElementInitializer("Button", function(frame, elementData)
        InitializeRecipeListRow(frame)
        frame:SetWidth(scrollBox:GetWidth())
        BindRecipeListRow(frame, elementData)
    end)

    ScrollUtil.InitScrollBoxListWithScrollBar(scrollBox, scrollBar, view)

    mainFrame.recipeScrollBox = scrollBox
    mainFrame.recipeScrollBar = scrollBar
    mainFrame.recipeScrollView = view

    return recipeFrame
end


-- ============================================================================
-- CREATE ALL PANELS
-- ============================================================================

function HDG_RecipesTab:Create()
    self:CreateRecipePanel()
    privateDB.HDG_QueuePanel.Create()
    privateDB.HDG_MaterialsPanel.Create()
end

-- ============================================================================
-- CRAFTING QUEUE FUNCTIONS
-- ============================================================================

function HDG_RecipesTab:AddToCraftingQueue(source, skipRefresh, quantity)
    local item = HDG_PowerCrafting.MakeQueueItem(source, quantity)
    if not item then return end
    HDG.Store:Dispatch("ADD_TO_CRAFTING_QUEUE", { item = item })
end

-- ============================================================================
-- REFRESH RECIPES (content area)
-- ============================================================================

function HDG_RecipesTab:RefreshRecipes()
    if not mainFrame or not mainFrame.recipeScrollBox then return end

    if mainFrame.UpdateCraftableButton then mainFrame.UpdateCraftableButton() end

    -- Per-refresh CanCraftRecipe cache cleared before building the DataProvider,
    -- not inside the initializer (the latter would rerun on every scroll-in).
    craftabilityCache = {}
    HideProfessionSectionHeader()
    privateDB.HDG_ProfessionSidebar.Refresh()

    local dp = CreateDataProvider()
    local playerProfessions = Data().GetPlayerProfessions()
    local isAllMode = (HDG.Store:GetCharacterProfessionFilter() == "All")
    local isCraftableMode = HDG.Store:GetConfig("craftableFilter") == true
    local filterCtxAll = BuildFilterContext()

    if #playerProfessions == 1 then
        UpdateProfessionSectionHeader(playerProfessions[1].name)
        if mainFrame.stickyHeader then mainFrame.stickyHeader:Hide() end
    else
        local allProfsLabel = L["FILTER_ALL_PROFESSIONS"] or "All Professions"
        UpdateProfessionSectionHeader(allProfsLabel)
        if mainFrame.stickyHeader then mainFrame.stickyHeader:Hide() end
    end

    if isAllMode and isCraftableMode then
        -- Unified tier list across all professions; no per-profession headers.
        local tier100, tier75to99, tier50to74, tierBelow50 = {}, {}, {}, {}
        for _, profData in ipairs(playerProfessions) do
            local profName = profData.name
            local hasProfession = HDG_Data.PlayerHasProfession(profName)
            for _, recipe in ipairs(profData.items) do
                if HDG_Data.RecipeMatchesFilters(recipe, filterCtxAll) then
                    local isKnown = HDG_Data.PlayerKnowsRecipeBySpellID(recipe.spellID)
                    local canCraft, maxQty, _, matPercent = GetCachedCraftability(recipe)
                    recipe._maxCraftable = maxQty
                    recipe._materialPercent = matPercent
                    recipe._noProfession = not hasProfession
                    recipe._professionName = profName
                    if hasProfession and isKnown and canCraft and maxQty > 0 then
                        table.insert(tier100, recipe)
                    elseif matPercent >= 75 then
                        table.insert(tier75to99, recipe)
                    elseif matPercent >= 50 then
                        table.insert(tier50to74, recipe)
                    elseif matPercent > 0 then
                        table.insert(tierBelow50, recipe)
                    end
                end
            end
        end
        table.sort(tier75to99, sortByPercent)
        table.sort(tier50to74, sortByPercent)
        table.sort(tierBelow50, sortByPercent)

        PushTierGroup(dp, tier100, L["RECIPE_TIER_READY"], HDG.Theme.C_Success(), true)
        PushTierGroup(dp, tier75to99, L["RECIPE_TIER_ALMOST"], HDG.Theme.C_Success(), true)
        PushTierGroup(dp, tier50to74, L["RECIPE_TIER_HALFWAY"], HDG.Theme.C_Warning(), true)
        PushTierGroup(dp, tierBelow50, L["RECIPE_TIER_NEED_MATS"], HDG.Theme.C_Error(), true)
    else
        for _, profData in ipairs(playerProfessions) do
            local showInlineHeader = isAllMode and not isCraftableMode
            local skipFixedHeader = isAllMode
            PushProfession(dp, profData, showInlineHeader, skipFixedHeader)
        end
    end

    -- RetainScrollPosition so click-to-add-to-queue (which cascades to
    -- HDG_UI.RefreshUI -> RefreshRecipes) doesn't jump the list back to top.
    -- ScrollBox clamps to data bounds automatically if the list shrinks.
    mainFrame.recipeScrollBox:SetDataProvider(dp, ScrollBoxConstants.RetainScrollPosition)
end

-- ============================================================================
-- MAIN REFRESH (called by HDG_UI.RefreshUI)
-- ============================================================================

function HDG_RecipesTab:Refresh()
    self:RefreshRecipes()
    privateDB.HDG_QueuePanel.Refresh()
    privateDB.HDG_MaterialsPanel.Refresh()
end

-- ============================================================================
-- SHOW/HIDE
-- ============================================================================

function HDG_RecipesTab:Show()
    if mainFrame.recipeFrame then mainFrame.recipeFrame:Show() end
    if mainFrame.queueFrame then mainFrame.queueFrame:Show() end
    if mainFrame.materialsFrame then mainFrame.materialsFrame:Show() end
    -- Initialize lumber layout based on pin state
    if HDG_UI and HDG_UI.UpdateLumberLayout then
        HDG_UI.UpdateLumberLayout()
    end
    self:Refresh()
end

function HDG_RecipesTab:Hide()
    if mainFrame.recipeFrame then mainFrame.recipeFrame:Hide() end
    if mainFrame.queueFrame then mainFrame.queueFrame:Hide() end
    if mainFrame.materialsFrame then mainFrame.materialsFrame:Hide() end
end

-- ============================================================================
-- EVENTBUS SUBSCRIPTIONS
-- Decouple UI refresh from direct function calls
-- ============================================================================

local function RegisterEventBusHandlers()
    if not HDG or not HDG.EventBus then return end

    HDG.EventBus:Register("HDG_STATE_CHANGED", function(payload)
        if not payload or not payload.action then return end

        local action = payload.action

        -- Crafting queue changes: refresh queue, materials, and recipe rows (in-queue badges)
        if action == "ADD_TO_CRAFTING_QUEUE" or
           action == "REMOVE_FROM_CRAFTING_QUEUE" or
           action == "UPDATE_CRAFTING_QUEUE_ITEM_QTY" or
           action == "CLEAR_CRAFTING_QUEUE" then
            privateDB.HDG_QueuePanel.Refresh()
            privateDB.HDG_MaterialsPanel.Refresh()
            HDG_RecipesTab:RefreshRecipes()
        end

        -- Config changes that affect recipe display
        if action == "SET_CONFIG" then
            -- Could check payload for specific config keys if needed
            HDG_RecipesTab:RefreshRecipes()
        end

        -- UI filter changes
        if action == "SET_UI" then
            HDG_RecipesTab:RefreshRecipes()
        end
    end)

    -- Collection data ready (async catalog search completed) — refresh visible tab
    HDG.EventBus:Register("HDG_COLLECTION_READY", function()
        if mainFrame and mainFrame:IsShown() then
            HDG_RecipesTab:RefreshRecipes()
        end
    end)
end

RegisterEventBusHandlers()

-- ============================================================================
-- EXPORT TO GLOBAL
-- ============================================================================

_G.HDG_RecipesTab = HDG_RecipesTab

return HDG_RecipesTab
