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

-- Module namespace
HDG_UI = {}

-- Local references
local CONFIG = privateDB.CONFIG
local L = HDG_L or {}  -- Localization

-- ============================================================================
-- 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

-- ============================================================================
-- 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 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

local function GetCraftingQueue()
    local state = GetState()
    return state and state.craftingQueue or {}
end

-- HDG_Data is global, so we can access it directly or via _G.HDG_Data
-- Check if HDG_Data is initialized, if not, use _G.HDG_Data
local HDG_Data = _G.HDG_Data
-- Note: privateDB.HDG_Data might be nil if initialized later, so best to rely on _G

-- UI State
local mainFrame
local activeTab = "recipes"
-- (recipesTabDirty removed — was set but never read)
local embeddedLumberState = nil -- Snapshot for early-exit in RefreshEmbeddedLumber

-- Orc quotes for footer (changes on PLAYER_ENTERING_WORLD)
local ORC_QUOTES = {
    "Zug zug.",
    "Lok'tar ogar!",
    "Me smash!",
    "Work is da poop!",
    "Blood and Thunder!",
    "Dabu.",
    "Time to kill?",
    "For the Horde!",
    "Me busy!",
    "Axe hungry.",
    "Crush them all!",
    "Why you poke me?",
    "Strength and Honor.",
    "Me not that kind of orc.",
    "You weak. Me strong.",
    "More wood? Ugh.",
    "Me hit it now?",
    "Victory or death!",
    "Small talk is for elves.",
    "Something need doing?",
}
local currentQuote = ORC_QUOTES[math.random(#ORC_QUOTES)]

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

-- ============================================================================
-- HELPER FUNCTIONS
-- ============================================================================

-- Use centralized ClearScrollContent from UIHelpers
local function ClearScrollContent(scrollContent)
    HDG_UIHelpers.ClearScrollContent(scrollContent)
end

local function CreateProgressBar(parent, have, needed, xPos, yPos, showText)
    if not CONFIG.PROGRESS_BAR.SHOW_BARS then return nil end

    local bar = CreateFrame("StatusBar", nil, parent, "BackdropTemplate")
    bar:SetSize(CONFIG.PROGRESS_BAR.WIDTH, CONFIG.PROGRESS_BAR.HEIGHT)
    bar:SetPoint("RIGHT", xPos or -60, yPos or 0)
    bar:SetStatusBarTexture(CONFIG.TEXTURES.STATUSBAR)
    bar:SetMinMaxValues(0, needed)
    bar:SetValue(math.min(have, needed))

    local percentage = needed > 0 and (have / needed * 100) or 0
    bar:SetStatusBarColor(HDG_UIHelpers.GetProgressBarColor(percentage))

    local scheme = GetScheme()
    local bg = bar:CreateTexture(nil, "BACKGROUND")
    bg:SetAllPoints(bar)
    bg:SetTexture(CONFIG.TEXTURES.STATUSBAR)
    bg:SetVertexColor(scheme.bg.r, scheme.bg.g, scheme.bg.b, 0.7)

    if showText then
        local text = bar:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        text:SetPoint("CENTER", 0, 0)
        text:SetJustifyH("CENTER")
        ApplyFont(text, scheme, "small", 10)
        if have >= needed then
            local extra = have - needed
            text:SetText(extra > 0 and string.format(HDG.Theme.C_Text() .. "%d/%d+%d|r", needed, needed, extra) or
                string.format(HDG.Theme.C_Text() .. "%d/%d|r", needed, needed))
        else
            text:SetText(string.format(HDG.Theme.C_Text() .. "%d/%d|r", have, needed))
        end
        bar.text = text
    end

    return bar
end

-- Helper: Count known recipes
function HDG_UI.CountKnownRecipes(profData)
    if not profData or not profData.items then return 0 end

    local knownCount = 0
    local dataModule = Data()
    local knowsBySpellID = dataModule.PlayerKnowsRecipeBySpellID
    local knowsByName = dataModule.PlayerKnowsRecipe

    for _, recipe in ipairs(profData.items) do
        local isKnown = false
        if recipe.spellID and recipe.spellID > 0 then
            isKnown = knowsBySpellID and knowsBySpellID(recipe.spellID)
        elseif knowsByName then
            isKnown = knowsByName(recipe.name)
        end

        if isKnown then
            knownCount = knownCount + 1
        end
    end
    return knownCount
end

-- CreateLumberRow is now provided by HDG_UIHelpers.CreateLumberRow

-- ============================================================================
-- UI CREATION
-- ============================================================================

function HDG_UI.CreateUI()
    if mainFrame then return end

    local scheme = GetScheme()

    mainFrame = CreateFrame("Frame", "HDG_MainFrame", UIParent, "BackdropTemplate")

    -- Default size (scaling now handled via UI Scale slider in settings)
    local defaultWidth = CONFIG.MAIN_FRAME_WIDTH
    local defaultHeight = CONFIG.MAIN_FRAME_HEIGHT

    mainFrame:SetSize(defaultWidth, defaultHeight)

    -- Restore saved layout (new system) or position (legacy) or default to center
    local state = HDG.Store and HDG.Store:GetState()
    local layout = state and state.layout and state.layout.MainFrame
    local savedPos = GetUIState("framePosition")

    if layout and layout.point then
        -- New layout system - restore position only (size controlled via UI Scale)
        mainFrame:SetPoint(layout.point, UIParent, layout.relPoint or layout.point, layout.x or 0, layout.y or 0)
    elseif savedPos and savedPos[1] and savedPos[3] and savedPos[4] then
        -- Legacy position system
        mainFrame:SetPoint(savedPos[1], UIParent, savedPos[2] or savedPos[1], savedPos[3], savedPos[4])
    else
        mainFrame:SetPoint("CENTER")
    end

    mainFrame:SetBackdrop({
        bgFile = CONFIG.TEXTURES.WHITE_BG,
        edgeFile = CONFIG.TEXTURES.WHITE_BG,  -- Solid 2px border (consistent with panels)
        tile = false,
        edgeSize = 1,
        insets = { left = 0, right = 0, top = 0, bottom = 0 }
    })
    mainFrame:SetBackdropColor(scheme.bg.r, scheme.bg.g, scheme.bg.b, scheme.bg.a)
    mainFrame:SetBackdropBorderColor(scheme.border.r, scheme.border.g, scheme.border.b, scheme.border.a)

    -- Atlas wood frame border (Housing Theme) - overlay frame for decorative border
    local borderFrame = CreateFrame("Frame", nil, mainFrame)
    borderFrame:SetAllPoints()
    borderFrame:SetFrameLevel(mainFrame:GetFrameLevel() + 10)  -- Render on top
    local borderTex = borderFrame:CreateTexture(nil, "OVERLAY")
    borderTex:SetAllPoints()
    local borderAtlas = scheme.atlas and scheme.atlas.windowBorder or "housing-simple-wood-frame"
    borderTex:SetAtlas(borderAtlas)
    borderTex:SetAlpha(1)
    borderFrame.tex = borderTex
    mainFrame.borderFrame = borderFrame
    -- Initial visibility based on theme
    if scheme.atlas and scheme.atlas.windowBorder then
        borderFrame:Show()
    else
        borderFrame:Hide()
    end

    -- Register with Theme Engine (MainWindow gets atlas bg + border for HousingTheme)
    RegisterWidget(mainFrame, "MainWindow")

    -- Apply saved UI scale
    local savedScale = state and state.config and state.config.scale or 1.0
    mainFrame:SetScale(math.max(0.5, math.min(2.0, savedScale)))

    mainFrame:SetFrameStrata("MEDIUM")
    mainFrame:SetToplevel(true)  -- Raise to top when clicked
    mainFrame:SetMovable(true)
    mainFrame:EnableMouse(true)
    mainFrame:RegisterForDrag("LeftButton")
    mainFrame:SetScript("OnDragStart", mainFrame.StartMoving)
    mainFrame:SetScript("OnDragStop", function(self)
        self:StopMovingOrSizing()
        local point, _, relativePoint, xOfs, yOfs = self:GetPoint()
        -- Legacy position system
        Dispatch("SET_FRAME_POSITION", { position = { point, relativePoint, xOfs, yOfs } })
        -- New layout system (includes size)
        HDG.Store:Dispatch("UPDATE_LAYOUT", {
            frame = "MainFrame",
            width = self:GetWidth(),
            height = self:GetHeight(),
            point = point,
            relPoint = relativePoint,
            x = xOfs,
            y = yOfs,
        })
    end)
    mainFrame:SetScript("OnShow", function()
        -- First-show-per-session: if user has pinned a default tab via
        -- right-clicking a tab button, switch to it once per session.
        -- _didApplyDefaultTab is module-local in-memory so it resets on
        -- /reload or game restart, exactly the "after login" semantics.
        if not HDG_UI._didApplyDefaultTab then
            HDG_UI._didApplyDefaultTab = true
            local cfg = HDG.Store and HDG.Store.state and HDG.Store.state.config
            local pinned = cfg and cfg.defaultTab
            if pinned and pinned ~= GetUIState("activeTab") then
                HDG_UI.SwitchTab(pinned)
            end
        end

        -- Same path as the Refresh button: invalidates the collection cache and
        -- re-runs the owned-items search. Replaces the old PEW+1s eager warm
        -- which fired before Blizzard's housing catalog was synced and locked
        -- the cache into a falsely-warmed empty state on cold login.
        HDG_UI.RefreshAll(true)

        -- Update pinned lumber widget's expand button when main window opens
        if HDG_LumberTracker and HDG_LumberTracker.IsPinnedVisible and HDG_LumberTracker.IsPinnedVisible() then
            local pinnedFrame = HDG_LumberTracker.GetPinnedFrame()
            if pinnedFrame and pinnedFrame.expandBtn and pinnedFrame.expandBtn.UpdateState then
                pinnedFrame.expandBtn.UpdateState()
            end
        end
    end)
    mainFrame:SetScript("OnHide", function()
        -- Update pinned lumber widget's expand button when main window closes
        if HDG_LumberTracker and HDG_LumberTracker.IsPinnedVisible and HDG_LumberTracker.IsPinnedVisible() then
            local pinnedFrame = HDG_LumberTracker.GetPinnedFrame()
            if pinnedFrame and pinnedFrame.expandBtn and pinnedFrame.expandBtn.UpdateState then
                pinnedFrame.expandBtn.UpdateState()
            end
        end
    end)
    mainFrame:Hide()
    table.insert(UISpecialFrames, "HDG_MainFrame")

    -- Initialize Trainers Module (needs mainFrame)
    if _G.HDG_Trainers and _G.HDG_Trainers.Init then
        _G.HDG_Trainers:Init(CONFIG, mainFrame, Data().PlayerHasProfession)
    end


    -- Title area background (HousingTheme atlas) - rotated 90 degrees, stretched horizontally
    local titleBgScheme = GetScheme()
    if titleBgScheme and titleBgScheme.atlas and titleBgScheme.atlas.titleBg then
        local titleBg = mainFrame:CreateTexture(nil, "BACKGROUND", nil, -7)
        titleBg:SetAtlas(titleBgScheme.atlas.titleBg)
        -- Rotate 90 degrees clockwise using 8-param SetTexCoord (UL, LL, UR, LR)
        titleBg:SetTexCoord(0, 1, 1, 1, 0, 0, 1, 0)
        -- After rotation, atlas width becomes our height
        local atlasInfo = C_Texture.GetAtlasInfo(titleBgScheme.atlas.titleBg)
        local nativeHeight = atlasInfo and atlasInfo.width or CONFIG.BANNER_HEIGHT
        titleBg:SetPoint("TOPLEFT", 0, 0)
        titleBg:SetPoint("TOPRIGHT", 0, 0)
        titleBg:SetHeight(nativeHeight)
        mainFrame.titleBgTexture = titleBg
    end

    -- Title textures - Vamoose's (left) + Housing Decor Guide (right)
    -- Images extend beyond frame edges for dramatic effect
    local title1Height = 38.25  -- Vamoose's (85% of 45)
    local title1Width = 400 * (title1Height / 89)  -- Maintain aspect ratio (400x89 source)
    local title2Height = 40.5  -- Housing Decor Guide (90% of original 45)
    local title2Width = 512 * (title2Height / 128)  -- Maintain aspect ratio (512x128 source)

    -- Create a title overlay frame that sits above borderFrame for all themes
    local titleOverlay = CreateFrame("Frame", nil, mainFrame)
    titleOverlay:SetAllPoints(mainFrame)
    titleOverlay:SetFrameLevel(mainFrame:GetFrameLevel() + 15)  -- Above borderFrame (which is +10)
    mainFrame.titleOverlay = titleOverlay

    mainFrame.title1Texture = titleOverlay:CreateTexture(nil, "OVERLAY", nil, 7)
    mainFrame.title1Texture:SetTexture(CONFIG.ADDON_TEXTURES.TITLE1)
    mainFrame.title1Texture:SetSize(title1Width, title1Height)
    mainFrame.title1Texture:SetPoint("TOPLEFT", mainFrame, "TOPLEFT", -10, 10)  -- Extend left of frame, down 2px
    mainFrame.title1Texture:SetVertexColor(0.8, 0.8, 0.8)  -- Dim 20%

    mainFrame.title2Texture = titleOverlay:CreateTexture(nil, "OVERLAY", nil, 7)
    mainFrame.title2Texture:SetTexture(CONFIG.ADDON_TEXTURES.TITLE2)
    mainFrame.title2Texture:SetSize(title2Width, title2Height)
    mainFrame.title2Texture:SetPoint("LEFT", mainFrame.title1Texture, "RIGHT", -2, -8)
    mainFrame.title2Texture:SetVertexColor(0.8, 0.8, 0.8)  -- Dim 20%

    -- Version text (bottom left of window)
    local titleScheme = GetScheme()
    mainFrame.titleText = mainFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    mainFrame.titleText:SetPoint("BOTTOMLEFT", 10, 8)
    ApplyFont(mainFrame.titleText, titleScheme, "small")
    mainFrame.titleText:SetText(string.format(HDG.Theme.C_Dim() .. "v%s|r", privateDB.ADDON_VERSION))

    -- Progress bar removed - totals now shown in profession tabs TOTAL row

    -- Close button
    local closeBtn = CreateFrame("Button", nil, mainFrame)
    closeBtn:SetSize(22, 22)
    closeBtn:SetPoint("TOPRIGHT", -6, -7)
    local closeIcon = closeBtn:CreateTexture(nil, "ARTWORK")
    closeIcon:SetSize(18, 18)
    closeIcon:SetPoint("CENTER")
    closeIcon:SetAtlas("XMarksTheSpot")
    closeIcon:SetVertexColor(0.7, 0.7, 0.7, 1)
    closeBtn.icon = closeIcon
    closeBtn:SetScript("OnClick", function() mainFrame:Hide() end)
    closeBtn:SetScript("OnEnter", function(self)
        self.icon:SetVertexColor(1, 1, 1, 1)
        GameTooltip:SetOwner(self, "ANCHOR_BOTTOM")
        GameTooltip:AddLine(L["TIP_CLOSE"], 1, 1, 1)
        GameTooltip:Show()
    end)
    closeBtn:SetScript("OnLeave", function(self)
        self.icon:SetVertexColor(0.7, 0.7, 0.7, 1)
        GameTooltip:Hide()
    end)

    -- Refresh button (sync atlas)
    local refreshBtn = CreateFrame("Button", nil, mainFrame)
    refreshBtn:SetSize(22, 22)
    refreshBtn:SetPoint("TOPRIGHT", closeBtn, "TOPLEFT", -4, 0)
    local refreshIcon = refreshBtn:CreateTexture(nil, "ARTWORK")
    refreshIcon:SetSize(22, 22)
    refreshIcon:SetPoint("CENTER")
    refreshIcon:SetAtlas("housefinder_neighborhood-party-sync-icon")
    refreshIcon:SetVertexColor(0.7, 0.7, 0.7, 1)  -- Dimmed when not hovered
    refreshBtn.icon = refreshIcon
    refreshBtn:SetScript("OnClick", function() HDG_UI.RefreshAll(false) end)
    refreshBtn:SetScript("OnEnter", function(self)
        self.icon:SetVertexColor(1, 1, 1, 1)
        GameTooltip:SetOwner(self, "ANCHOR_BOTTOM")
        GameTooltip:AddLine(L["TIP_REFRESH"], 1, 1, 1)
        GameTooltip:AddLine(L["TIP_REFRESH_DESC"], 0.7, 0.7, 0.7, true)
        GameTooltip:Show()
    end)
    refreshBtn:SetScript("OnLeave", function(self)
        self.icon:SetVertexColor(0.7, 0.7, 0.7, 1)
        GameTooltip:Hide()
    end)
    mainFrame.refreshBtn = refreshBtn

    -- Theme switcher button (alterations atlas)
    local themeBtn = CreateFrame("Button", nil, mainFrame)
    themeBtn:SetSize(22, 22)
    themeBtn:SetPoint("TOPRIGHT", refreshBtn, "TOPLEFT", -4, 0)
    themeBtn:RegisterForClicks("LeftButtonUp", "RightButtonUp")

    local themeIcon = themeBtn:CreateTexture(nil, "ARTWORK")
    themeIcon:SetAllPoints()
    themeIcon:SetAtlas("housing-dyable-palette-icon")
    themeIcon:SetVertexColor(0.7, 0.7, 0.7, 1)  -- Dimmed when not hovered
    themeBtn.icon = themeIcon

    themeBtn:SetScript("OnMouseDown", function() themeIcon:SetVertexColor(0.5, 0.5, 0.5, 1) end)
    themeBtn:SetScript("OnMouseUp", function() themeIcon:SetVertexColor(0.7, 0.7, 0.7, 1) end)

    -- Helper to show/update theme tooltip
    local function ShowThemeTooltip(btn)
        local HDG = _G.HousingDecorGuide
        local currentTheme = GetConfig("theme") or "SolarizedDark"
        local currentName = (HDG and HDG.Colors and HDG.Colors.ThemeNames[currentTheme]) or currentTheme
        local nextTheme = HDG.Colors:GetNextTheme(currentTheme)
        local prevTheme = HDG.Colors:GetPreviousTheme(currentTheme)
        local nextName = HDG.Colors.ThemeNames[nextTheme] or nextTheme
        local prevName = HDG.Colors.ThemeNames[prevTheme] or prevTheme

        GameTooltip:ClearLines()
        GameTooltip:SetOwner(btn, "ANCHOR_BOTTOM")
        GameTooltip:AddLine(L["TIP_CHANGE_THEME"], 1, 1, 1)
        GameTooltip:AddLine(string.format(L["TIP_THEME_CURRENT"], currentName), 0.7, 0.7, 0.7, true)
        GameTooltip:AddLine(" ")
        GameTooltip:AddLine(string.format(L["TIP_THEME_LEFTCLICK"], nextName), 0.5, 0.8, 0.5, true)
        GameTooltip:AddLine(string.format(L["TIP_THEME_RIGHTCLICK"], prevName), 0.8, 0.5, 0.5, true)
        GameTooltip:Show()
    end

    themeBtn:SetScript("OnClick", function(self, button)
        local HDG = _G.HousingDecorGuide
        if not HDG or not HDG.Colors then return end

        local currentTheme = GetConfig("theme") or "SolarizedDark"
        local newTheme

        if button == "RightButton" then
            newTheme = HDG.Colors:GetPreviousTheme(currentTheme)
        else
            newTheme = HDG.Colors:GetNextTheme(currentTheme)
        end

        -- Dispatch to Store
        Dispatch("SET_THEME", { theme = newTheme })

        -- Trigger theme update event for ThemeEngine
        if HDG.EventBus then
            HDG.EventBus:Trigger("HDG_THEME_UPDATE", { themeName = newTheme })
        end

        -- Print notification
        local themeName = HDG.Colors.ThemeNames[newTheme] or newTheme
        print(HDG.Theme.C_Accent() .. "[HDG]|r Theme: " .. HDG.Theme.C_Warning() .. themeName .. "|r")

        -- Update tooltip if still hovering
        if GameTooltip:IsShown() and GameTooltip:GetOwner() == self then
            ShowThemeTooltip(self)
        end
    end)

    themeBtn:SetScript("OnEnter", function(self)
        self.icon:SetVertexColor(1, 1, 1, 1)
        ShowThemeTooltip(self)
    end)

    themeBtn:SetScript("OnLeave", function(self)
        self.icon:SetVertexColor(0.7, 0.7, 0.7, 1)
        GameTooltip:Hide()
    end)

    mainFrame.themeBtn = themeBtn

    -- Materials list button (icon button, to the left of theme button)
    local materialsBtn = CreateFrame("Button", nil, mainFrame)
    materialsBtn:SetSize(100, 22)
    materialsBtn:SetPoint("RIGHT", themeBtn, "LEFT", -4, 0)
    local matNormal = materialsBtn:CreateTexture(nil, "BACKGROUND")
    matNormal:SetAllPoints()
    matNormal:SetAtlas("UI-DialogBox-Button-Up")
    materialsBtn:SetNormalTexture(matNormal)
    local matPressed = materialsBtn:CreateTexture(nil, "BACKGROUND")
    matPressed:SetAllPoints()
    matPressed:SetAtlas("UI-DialogBox-Button-Down")
    materialsBtn:SetPushedTexture(matPressed)
    local matHighlight = materialsBtn:CreateTexture(nil, "BACKGROUND")
    matHighlight:SetAllPoints()
    matHighlight:SetAtlas("UI-DialogBox-Button-Highlight")
    materialsBtn:SetHighlightTexture(matHighlight)
    local matIcon = materialsBtn:CreateTexture(nil, "OVERLAY")
    matIcon:SetSize(14, 14)
    matIcon:SetPoint("LEFT", 8, 0)
    matIcon:SetTexture("Interface\\MINIMAP\\TRACKING\\Banker")
    local matText = materialsBtn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    matText:SetPoint("LEFT", matIcon, "RIGHT", 4, 0)
    matText:SetPoint("RIGHT", -8, 0)
    matText:SetJustifyH("LEFT")
    matText:SetTextColor(1, 1, 1, 1)
    matText:SetText(L["HEADER_MATERIALS"])
    materialsBtn:SetScript("OnClick", function()
        if HDG_MaterialsList then HDG_MaterialsList.Toggle() end
    end)
    materialsBtn:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, "ANCHOR_BOTTOM")
        GameTooltip:AddLine(L["TIP_MATERIALS_LIST"], 1, 1, 1)
        GameTooltip:AddLine(L["TIP_MATERIALS_LIST_DESC"], 0.7, 0.7, 0.7, true)
        GameTooltip:Show()
    end)
    materialsBtn:SetScript("OnLeave", function()
        GameTooltip:Hide()
    end)
    mainFrame.materialsBtn = materialsBtn

    -- Shopping list button (red atlas button, to the left of icon row)
    local shopListBtn = CreateFrame("Button", nil, mainFrame)
    shopListBtn:SetSize(118, 22)
    shopListBtn:SetPoint("RIGHT", materialsBtn, "LEFT", -8, 0)
    local slNormal = shopListBtn:CreateTexture(nil, "BACKGROUND")
    slNormal:SetAllPoints()
    slNormal:SetAtlas("UI-DialogBox-Button-Up")
    shopListBtn:SetNormalTexture(slNormal)
    local slPressed = shopListBtn:CreateTexture(nil, "BACKGROUND")
    slPressed:SetAllPoints()
    slPressed:SetAtlas("UI-DialogBox-Button-Down")
    shopListBtn:SetPushedTexture(slPressed)
    local slHighlight = shopListBtn:CreateTexture(nil, "BACKGROUND")
    slHighlight:SetAllPoints()
    slHighlight:SetAtlas("UI-DialogBox-Button-Highlight")
    shopListBtn:SetHighlightTexture(slHighlight)
    local slIcon = shopListBtn:CreateTexture(nil, "OVERLAY")
    slIcon:SetSize(14, 14)
    slIcon:SetPoint("LEFT", 8, 0)
    slIcon:SetAtlas("Perks-ShoppingCart")
    local slText = shopListBtn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    slText:SetPoint("LEFT", slIcon, "RIGHT", 4, 0)
    slText:SetPoint("RIGHT", -8, 0)
    slText:SetJustifyH("LEFT")
    slText:SetTextColor(1, 1, 1, 1)
    slText:SetText(L["HEADER_SHOPPING_LIST"] or "Shopping List")
    shopListBtn.slText = slText
    shopListBtn.slIcon = slIcon
    shopListBtn:SetScript("OnClick", function()
        if HDG_VendorShoppingList then HDG_VendorShoppingList.Toggle() end
    end)
    shopListBtn:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, "ANCHOR_BOTTOM")
        GameTooltip:AddLine(L["HEADER_SHOPPING_LIST"] or "Shopping List", 1, 1, 1)
        GameTooltip:AddLine(L["TIP_SHIFTCLICK_ADD_ITEMS"], 0.7, 0.7, 0.7, true)
        GameTooltip:Show()
    end)
    shopListBtn:SetScript("OnLeave", function()
        GameTooltip:Hide()
    end)
    mainFrame.shopListBtn = shopListBtn

    -- Housing level badge (clickable house chest with level overlay).
    -- Animations: idle scale-pulse + periodic sparkle flipbook to draw the eye
    -- so players discover it as a clickable "go to House tab" affordance.
    local ringContainer = CreateFrame("Button", nil, mainFrame)
    ringContainer:SetSize(36, 36)
    ringContainer:SetPoint("LEFT", mainFrame.title2Texture, "RIGHT", 6, -2)

    -- Frame ring (chest plinth backdrop)
    local ringBg = ringContainer:CreateTexture(nil, "BACKGROUND")
    ringBg:SetAtlas("housing-dashboard-fillbar-icon-bg")
    ringBg:SetAllPoints(ringContainer)

    -- Chest icon (the actual "loot for visiting House tab" semantic)
    local chestIcon = ringContainer:CreateTexture(nil, "ARTWORK")
    chestIcon:SetAtlas("house-chest-icon")
    chestIcon:SetSize(22, 22)
    chestIcon:SetPoint("CENTER", 0, 1)

    -- Level number — bottom-right corner, gold with shadow
    local ringLevel = ringContainer:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    ringLevel:SetPoint("BOTTOMRIGHT", -3, 2)
    ringLevel:SetTextColor(1.0, 0.82, 0.29, 1)
    ringLevel:SetShadowOffset(1, -1)
    ringLevel:SetShadowColor(0, 0, 0, 1)

    -- Frame border on top
    local ringFrameTex = ringContainer:CreateTexture(nil, "BORDER")
    ringFrameTex:SetAtlas("housing-dashboard-fillbar-icon-frame")
    ringFrameTex:SetAllPoints(ringContainer)

    -- Sparkle flipbook overlay (Blizzard's level-up celebration anim).
    -- Plays on a loop every ~6s when idle to attract attention.
    local sparkle = ringContainer:CreateTexture(nil, "OVERLAY")
    sparkle:SetAtlas("housing-dashboard-fillbar-icon-fx-flipbook", false)
    sparkle:SetSize(60, 56)
    sparkle:SetPoint("CENTER", 0, 2)
    sparkle:SetAlpha(0)

    local sparkleAg = sparkle:CreateAnimationGroup()
    sparkleAg:SetLooping("REPEAT")
    -- Wait then sparkle: 6 seconds idle, then 1 second of flipbook
    local pause = sparkleAg:CreateAnimation("Alpha")
    pause:SetFromAlpha(0); pause:SetToAlpha(0); pause:SetDuration(6); pause:SetOrder(1)
    local fadeIn = sparkleAg:CreateAnimation("Alpha")
    fadeIn:SetFromAlpha(0); fadeIn:SetToAlpha(1); fadeIn:SetDuration(0.1); fadeIn:SetOrder(2)
    local flip = sparkleAg:CreateAnimation("Flipbook")
    flip:SetDuration(1.0); flip:SetFlipBookColumns(5); flip:SetFlipBookRows(8); flip:SetOrder(2)
    local fadeOut = sparkleAg:CreateAnimation("Alpha")
    fadeOut:SetFromAlpha(1); fadeOut:SetToAlpha(0); fadeOut:SetDuration(0.2); fadeOut:SetOrder(3)
    -- Stop after 3 sparkles — enough to catch the eye on first sight, then quiet down
    sparkleAg._cyclesLeft = 3
    sparkleAg:SetScript("OnLoop", function(self)
        self._cyclesLeft = (self._cyclesLeft or 0) - 1
        if self._cyclesLeft <= 0 then self:Stop() end
    end)
    sparkleAg:Play()

    ringContainer.levelText = ringLevel
    ringContainer.sparkleAg = sparkleAg

    ringContainer:RegisterForClicks("LeftButtonUp", "RightButtonUp")
    ringContainer:SetScript("OnEnter", function(self)
        chestIcon:SetScale(1.15)
        if self.tooltipText then
            GameTooltip:SetOwner(self, "ANCHOR_BOTTOM")
            GameTooltip:AddLine(self.tooltipText, 1, 1, 1)
            GameTooltip:AddLine("Click for House Stats", 0.7, 0.7, 0.7)
            local cfg = HDG.Store and HDG.Store.state and HDG.Store.state.config
            local isPinned = cfg and cfg.defaultTab == "house"
            GameTooltip:AddLine(" ")
            if isPinned then
                GameTooltip:AddLine(HDG.Theme.C_Success() ..
                    (L["TIP_DEFAULT_TAB_PINNED"] or "Default tab on login. Right-click to clear.") .. "|r",
                    0.7, 0.7, 0.7)
            else
                GameTooltip:AddLine(HDG.Theme.C_Dim() ..
                    (L["TIP_DEFAULT_TAB_UNPINNED"] or "Right-click to make this the default tab on login.") .. "|r",
                    0.7, 0.7, 0.7)
            end
            GameTooltip:Show()
        end
    end)
    ringContainer:SetScript("OnLeave", function()
        chestIcon:SetScale(1)
        GameTooltip:Hide()
    end)
    ringContainer:SetScript("OnClick", function(_, button)
        if button == "RightButton" then
            local cfg = HDG.Store and HDG.Store.state and HDG.Store.state.config
            if cfg then
                if cfg.defaultTab == "house" then
                    cfg.defaultTab = nil
                    print(HDG.Theme.C_Accent() .. "[HDG]|r " ..
                        (L["DEFAULT_TAB_CLEARED"] or "Default tab cleared. HDG will open on the last-used tab."))
                else
                    cfg.defaultTab = "house"
                    print(HDG.Theme.C_Accent() .. "[HDG]|r " ..
                        string.format(L["DEFAULT_TAB_SET"] or "Default tab set: %s. HDG will open here on first show after login.",
                            "House"))
                end
                if HDG.Store.QueueSave then HDG.Store:QueueSave() end
            end
            return
        end
        HDG_UI.SwitchTab("house")
    end)
    ringContainer:Hide()
    mainFrame.houseLevelRing = ringContainer

    -- Title row separator line
    local titleSeparator = mainFrame:CreateTexture(nil, "ARTWORK")
    titleSeparator:SetHeight(1)
    titleSeparator:SetPoint("TOPLEFT", 8, -(CONFIG.BANNER_HEIGHT + 2))
    titleSeparator:SetPoint("TOPRIGHT", -8, -(CONFIG.BANNER_HEIGHT + 2))
    local sepScheme = GetScheme()
    if sepScheme then
        titleSeparator:SetColorTexture(sepScheme.border.r, sepScheme.border.g, sepScheme.border.b, 0.6)
    else
        titleSeparator:SetColorTexture(0.4, 0.4, 0.4, 0.6)
    end
    RegisterWidget(titleSeparator, "Separator")

    -- Tab System
    local tabContainer = CreateFrame("Frame", nil, mainFrame)
    tabContainer:SetPoint("TOPLEFT", 15, -(CONFIG.BANNER_HEIGHT + 2))
    tabContainer:SetPoint("TOPRIGHT", -15, -(CONFIG.BANNER_HEIGHT + 2))
    tabContainer:SetHeight(CONFIG.TAB_HEIGHT)
    mainFrame.tabContainer = tabContainer

    -- Wood sign background for tab bar (HousingTheme only)
    -- Full window width, positioned at tab bar height
    local tabBgTexture = mainFrame:CreateTexture(nil, "ARTWORK", nil, -7)
    tabBgTexture:SetPoint("TOPLEFT", 0, -(CONFIG.BANNER_HEIGHT + 2))
    tabBgTexture:SetPoint("TOPRIGHT", 0, -(CONFIG.BANNER_HEIGHT + 2))
    tabBgTexture:SetHeight(CONFIG.TAB_HEIGHT)
    tabBgTexture:Hide()
    mainFrame.tabBgTexture = tabBgTexture

    -- Apply atlas if HousingTheme
    local tabBgScheme = GetScheme()
    if tabBgScheme and tabBgScheme.atlas and tabBgScheme.atlas.titleWoodSign then
        tabBgTexture:SetAtlas(tabBgScheme.atlas.titleWoodSign)
        tabBgTexture:Show()
    end

    local tabData = {
        { name = L["TAB_RECIPES"] or "Recipes",   key = "recipes",     side = "left",
          atlasScale = math.floor((CONFIG.TAB_HEIGHT - 8) * 1.3), extraGap = 6,
          atlas = { up = "UI-HUD-MicroMenu-SpellbookAbilities-Down", over = "UI-HUD-MicroMenu-SpellbookAbilities-Mouseover", down = "UI-HUD-MicroMenu-SpellbookAbilities-Up" } },
        { name = L["TAB_ACQUIRE"] or "Acquire",    key = "acquisition", side = "left",
          atlas = { up = "crosshair_unabledirections_32", over = "crosshair_directions_32", down = "crosshair_directions_32" } },
        { name = L["TAB_PREVIEW"] or "Preview",   key = "preview",     side = "left",
          atlas = { up = "decor-controls-inspect-default", over = "decor-controls-inspect-pressed", down = "decor-controls-inspect-active" } },
        { name = L["TAB_GOBLIN"] or "Goblin",     key = "goblin",      side = "left", singleAtlas = "majorfactions_icons_stars512" },
        { name = L["TAB_TRAINERS"] or "Trainers", key = "trainers",    side = "left", singleAtlas = "adventureguide-icon-whatsnew" },
        { name = L["TAB_ALTS"] or "Alts",         key = "characters",  side = "left", singleAtlas = "trios-icon-bubble-default", atlasScale = (CONFIG.TAB_HEIGHT - 6) * 2, extraGap = 8, textXOffset = 16 },
        { name = L["TAB_STYLES"] or "Style",         key = "styles",      side = "left", singleAtlas = "Quest-Alliance-WaxSeal" },
        -- Projects tab hidden for release; in-development. Module still loads
        -- via TOC + lifecycle hooks below; just no button on the tab bar.
        -- { name = L["TAB_PROJECTS"] or "Projects",    key = "projects",    side = "left", singleAtlas = "ParagonReputation_Bag" },
        { name = L["TAB_DATA"] or "Data",         key = "data",        side = "right", iconOnly = true,
          atlas = { up = "decor-placement-list-default", over = "decor-placement-list-active", down = "decor-placement-list-pressed" } },
        { name = L["TAB_CONFIG"] or "Config",     key = "config",      side = "right", iconOnly = true },
        -- Mogul anchored last among right-side tabs so it ends up LEFTMOST
        -- of the right group (right-side layout order: first processed =
        -- rightmost, last processed = leftmost). Visible: Mogul | Config | Data.
        { name = L["TAB_MOGUL"] or "Mogul",       key = "mogul",       side = "right", iconOnly = true,
          atlas = { up = "Mobile-TreasureIcon-Desaturated", over = "Mobile-TreasureIcon", down = "Mobile-TreasureIcon" },
          atlasScale = (CONFIG.TAB_HEIGHT - 6) * 0.9 },
    }

    local tabs = {}
    local leftOffset = 0
    local rightOffset = 7  -- right-edge padding before the first right-side tab
    local initScheme = GetScheme()
    for i, tabInfo in ipairs(tabData) do
        local tabWidth = tabInfo.iconOnly and CONFIG.TAB_ICON_ONLY_WIDTH or CONFIG.TAB_TEXT_WIDTH
        local tabBar = CreateFrame("Frame", nil, tabContainer, "BackdropTemplate")
        tabBar:SetSize(tabWidth, CONFIG.TAB_HEIGHT - 3)

        -- tabInfo.extraGap is additive on top of CONFIG.TAB_GAP, applied to
        -- the gap that follows this tab (so set it on the LEFT tab of a
        -- pair). Negative values tighten the gap.
        local thisGap = CONFIG.TAB_GAP + (tabInfo.extraGap or 0)
        if tabInfo.side == "right" then
            tabBar:SetPoint("RIGHT", -rightOffset, 0)
            rightOffset = rightOffset + tabWidth + thisGap
        else
            tabBar:SetPoint("LEFT", leftOffset, 0)
            leftOffset = leftOffset + tabWidth + thisGap
        end

        tabBar.key = tabInfo.key

        -- No backdrop border on tabs
        tabBar:SetBackdrop(nil)

        -- Icon
        local icon = tabBar:CreateTexture(nil, "OVERLAY")
        tabBar.icon = icon

        if tabInfo.singleAtlas then
            -- Single-atlas icon: alpha-based states, no border
            local saSize = tabInfo.atlasScale or (CONFIG.TAB_HEIGHT - 6)
            icon:SetSize(saSize, saSize)
            icon:SetAtlas(tabInfo.singleAtlas)
            icon:SetAlpha(0.8)
            tabBar._isAtlasIcon = true
            if tabInfo.iconOnly then
                icon:SetPoint("CENTER", 0, tabInfo.iconYOffset or 0)
                tabBar:SetSize(saSize + 4, CONFIG.TAB_HEIGHT - 3)
                tabBar.text = nil
            else
                icon:SetPoint("LEFT", 2, 0)
                tabBar.text = tabBar:CreateFontString(nil, "OVERLAY", "GameFontNormal")
                -- Fixed offset from tab LEFT (not icon:RIGHT) so oversized
                -- icons like Alts at 2x don't push text out of column.
                -- Per-tab textXOffset can nudge specific labels (e.g. Alts
                -- text needs +10 to clear its 2x icon).
                local TEXT_LEFT_OFFSET = 2 + (CONFIG.TAB_HEIGHT - 6) + 2 + (tabInfo.textXOffset or 0)
                tabBar.text:SetPoint("LEFT", tabBar, "LEFT", TEXT_LEFT_OFFSET, 0)
                ApplyFont(tabBar.text, initScheme, "small", 11)
                tabBar.text:SetText(tabInfo.name)
            end
        elseif tabInfo.key == "config" then
            -- Config tab: atlas gear icon with 3 named variants
            local gearSize = CONFIG.TAB_HEIGHT - 6
            icon:SetSize(gearSize, gearSize)
            icon:SetPoint("CENTER", 0, 0)
            icon:SetAtlas("decor-controls-settings-default")
            icon:SetAlpha(0.8)
            tabBar:SetSize(gearSize + 4, CONFIG.TAB_HEIGHT - 3)
            tabBar._configAtlas = true
            tabBar.text = nil
        elseif tabInfo.atlas then
            -- Atlas-based icon (no border)
            local atlasSize = tabInfo.atlasScale or (tabInfo.iconOnly and (CONFIG.TAB_HEIGHT - 6) or (CONFIG.TAB_HEIGHT - 8))
            local atlasH = tabInfo.atlasHeight or atlasSize
            icon:SetSize(atlasSize, atlasH)
            icon:SetAtlas(tabInfo.atlas.up)
            icon:SetAlpha(tabInfo.iconOnly and 0.8 or 0.7)
            tabBar._atlasStates = tabInfo.atlas
            if tabInfo.iconOnly then
                icon:SetPoint("CENTER", 0, 0)
                tabBar:SetSize(atlasSize + 4, CONFIG.TAB_HEIGHT - 3)
                tabBar.text = nil
            else
                icon:SetPoint("LEFT", 2, 0)
                tabBar.text = tabBar:CreateFontString(nil, "OVERLAY", "GameFontNormal")
                -- Fixed offset from tab LEFT (not icon:RIGHT) so oversized
                -- icons like Alts at 2x don't push text out of column.
                -- Per-tab textXOffset can nudge specific labels (e.g. Alts
                -- text needs +10 to clear its 2x icon).
                local TEXT_LEFT_OFFSET = 2 + (CONFIG.TAB_HEIGHT - 6) + 2 + (tabInfo.textXOffset or 0)
                tabBar.text:SetPoint("LEFT", tabBar, "LEFT", TEXT_LEFT_OFFSET, 0)
                ApplyFont(tabBar.text, initScheme, "small", 11)
                tabBar.text:SetText(tabInfo.name)
            end
        elseif tabInfo.iconOnly then
            icon:SetSize(CONFIG.TAB_ICON_SIZE, CONFIG.TAB_ICON_SIZE)
            icon:SetTexture(CONFIG.TAB_ICONS[tabInfo.key])
            icon:SetAlpha(0.6)
            icon:SetPoint("CENTER", 0, 0)
            tabBar.text = nil
        else
            icon:SetSize(CONFIG.TAB_ICON_SIZE, CONFIG.TAB_ICON_SIZE)
            icon:SetTexture(CONFIG.TAB_ICONS[tabInfo.key])
            icon:SetAlpha(0.6)
            icon:SetPoint("LEFT", 4, 0)
            tabBar.text = tabBar:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            tabBar.text:SetPoint("LEFT", icon, "RIGHT", 4, 0)
            ApplyFont(tabBar.text, initScheme, "small", 11)
            tabBar.text:SetText(tabInfo.name)
        end

        -- 2px accent underline at bottom (hidden by default)
        local activeBar = tabBar:CreateTexture(nil, "OVERLAY")
        activeBar:SetHeight(2)
        activeBar:SetPoint("BOTTOMLEFT", 1, 1)
        activeBar:SetPoint("BOTTOMRIGHT", -1, 1)
        if initScheme then
            activeBar:SetColorTexture(initScheme.accent.r, initScheme.accent.g, initScheme.accent.b, 1)
        else
            activeBar:SetColorTexture(0.08, 0.72, 0.65, 1)
        end
        activeBar:Hide()
        tabBar.activeBar = activeBar

        -- Register ALL tabs for theme updates
        RegisterWidget(tabBar, "MainTab")

        -- Click overlay (left = switch, right = pin as default tab on login)
        local clickButton = CreateFrame("Button", nil, tabBar)
        clickButton:SetAllPoints(tabBar)
        clickButton:RegisterForClicks("LeftButtonUp", "RightButtonUp")
        clickButton:SetScript("OnClick", function(_, button)
            if button == "RightButton" then
                local cfg = HDG.Store and HDG.Store.state and HDG.Store.state.config
                if cfg then
                    if cfg.defaultTab == tabInfo.key then
                        cfg.defaultTab = nil
                        print(HDG.Theme.C_Accent() .. "[HDG]|r " ..
                            (L["DEFAULT_TAB_CLEARED"] or "Default tab cleared. HDG will open on the last-used tab."))
                    else
                        cfg.defaultTab = tabInfo.key
                        print(HDG.Theme.C_Accent() .. "[HDG]|r " ..
                            string.format(L["DEFAULT_TAB_SET"] or "Default tab set: %s. HDG will open here on first show after login.",
                                tabInfo.name or tabInfo.key))
                    end
                    if HDG.Store.QueueSave then HDG.Store:QueueSave() end
                end
                return
            end
            HDG_UI.SwitchTab(tabInfo.key)
        end)

        -- Tooltip
        local tooltipTexts = {
            recipes = L["TIP_TAB_RECIPES"] or "View all housing decor recipes",
            goblin = L["TIP_TAB_GOBLIN"] or "Profit analysis - find the most profitable crafts",
            mogul  = L["TIP_TAB_MOGUL"]  or "Mogul - best craft for gold or collection",
            preview = L["TIP_TAB_PREVIEW"] or "Preview decorations",
            acquisition = L["TIP_TAB_ACQUIRE"] or "Vendor shopping guide - find where to buy decorations",
            trainers = L["TIP_TAB_TRAINERS"] or "Find recipe trainers",
            characters = L["TIP_TAB_ALTS"] or "View profession data across all characters",
            data = L["TIP_TAB_DATA"] or "View farming history and statistics",
            config = L["TIP_TAB_CONFIG"] or "Configure addon settings and minimap icon",
        }

        clickButton:SetScript("OnEnter", function(self)
            if GetUIState("activeTab") ~= tabInfo.key then
                local s = tabBar._scheme or GetScheme()
                if tabBar._configAtlas then
                    tabBar.icon:SetAtlas("decor-controls-settings-pressed")
                elseif tabBar._isAtlasIcon then
                    tabBar.icon:SetAlpha(1.0)
                    if tabBar.text and s then tabBar.text:SetTextColor(s.text.r, s.text.g, s.text.b, s.text.a) end
                elseif tabBar._atlasStates then
                    tabBar.icon:SetAtlas(tabBar._atlasStates.over)
                    if tabBar.text and s then tabBar.text:SetTextColor(s.text.r, s.text.g, s.text.b, s.text.a) end
                else
                    if s then
                        tabBar:SetBackdropColor(s.panel.r, s.panel.g, s.panel.b, 0.4)
                    end
                    if tabBar.icon then tabBar.icon:SetAlpha(0.85) end
                    if tabBar.text and s then
                        tabBar.text:SetTextColor(s.text.r, s.text.g, s.text.b, s.text.a)
                    end
                end
            end
            GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
            GameTooltip:AddLine(HDG.Theme.C_Warning() .. tabInfo.name .. "|r", 1, 1, 1)
            GameTooltip:AddLine(tooltipTexts[tabInfo.key], 1, 1, 1)
            local cfg = HDG.Store and HDG.Store.state and HDG.Store.state.config
            local isPinned = cfg and cfg.defaultTab == tabInfo.key
            GameTooltip:AddLine(" ")
            if isPinned then
                GameTooltip:AddLine(HDG.Theme.C_Success() ..
                    (L["TIP_DEFAULT_TAB_PINNED"] or "Default tab on login. Right-click to clear.") .. "|r",
                    0.7, 0.7, 0.7)
            else
                GameTooltip:AddLine(HDG.Theme.C_Dim() ..
                    (L["TIP_DEFAULT_TAB_UNPINNED"] or "Right-click to make this the default tab on login.") .. "|r",
                    0.7, 0.7, 0.7)
            end
            GameTooltip:Show()
        end)

        clickButton:SetScript("OnLeave", function(self)
            if GetUIState("activeTab") ~= tabInfo.key then
                local s = tabBar._scheme or GetScheme()
                if tabBar._configAtlas then
                    tabBar.icon:SetAtlas("decor-controls-settings-default")
                elseif tabBar._isAtlasIcon then
                    tabBar.icon:SetAlpha(0.8)
                    if tabBar.text and s then tabBar.text:SetTextColor(s.text_dim.r, s.text_dim.g, s.text_dim.b, s.text_dim.a) end
                elseif tabBar._atlasStates then
                    tabBar.icon:SetAtlas(tabBar._atlasStates.up)
                    if tabBar.text and s then tabBar.text:SetTextColor(s.text_dim.r, s.text_dim.g, s.text_dim.b, s.text_dim.a) end
                else
                    if s then
                        tabBar:SetBackdropColor(s.bg.r, s.bg.g, s.bg.b, 0.3)
                    end
                    if tabBar.icon then tabBar.icon:SetAlpha(0.6) end
                    if tabBar.text and s then
                        tabBar.text:SetTextColor(s.text_dim.r, s.text_dim.g, s.text_dim.b, s.text_dim.a)
                    end
                end
            end
            GameTooltip:Hide()
        end)

        tabBar.button = clickButton
        table.insert(tabs, tabBar)
    end
    mainFrame.tabs = tabs

    -- Initialize tab modules
    HDG_LumberTracker.Init(mainFrame)

    -- Initialize tab modules with mainFrame reference
    if privateDB.HDG_RecipesTab then
        privateDB.HDG_RecipesTab:Init(mainFrame)
        privateDB.HDG_RecipesTab:Create()
    end

    if privateDB.HDG_AltsTab then
        privateDB.HDG_AltsTab.Init(mainFrame)
        privateDB.HDG_AltsTab.Create()
    end

    if privateDB.HDG_ConfigTab then
        privateDB.HDG_ConfigTab.Init(mainFrame)
        privateDB.HDG_ConfigTab.Create()
    end

    if privateDB.HDG_DataTab then
        privateDB.HDG_DataTab.Init(mainFrame)
        privateDB.HDG_DataTab.CreatePanel()
    end

    if privateDB.HDG_GoblinTab then
        privateDB.HDG_GoblinTab.Init(mainFrame)
        privateDB.HDG_GoblinTab.Create()
    end

    if privateDB.HDG_MogulTab then
        privateDB.HDG_MogulTab.Init(mainFrame)
    end

    -- House tab: init only (lazy create on first show)
    if privateDB.HDG_HouseTab then
        privateDB.HDG_HouseTab.Init(mainFrame)
    end

    -- Trainers placeholder frame (actual content handled by HDG_Trainers module)
    HDG_UI.CreateTrainersPanel()

    -- Initialize state
    if not GetUIState("activeTab") then
        Dispatch("SET_ACTIVE_TAB", { tab = "recipes" })
    end
    HDG_UI.UpdateTabs()

    -- Listen for theme changes to refresh tab colors
    HDG.EventBus:Register("HDG_THEME_UPDATE", function()
        HDG_UI.UpdateTabs()
    end)

    -- Footer with random Orc quote (after version text)
    local footerScheme = GetScheme()
    local footer = mainFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    footer:SetPoint("BOTTOMLEFT", 65, 8)  -- After version text
    footer:SetWidth(CONFIG.MAIN_FRAME_WIDTH - 330)  -- Leave room for house level on right
    footer:SetJustifyH("LEFT")
    footer:SetText(string.format(HDG.Theme.C_Accent() .. "Vamoose says:|r " .. HDG.Theme.C_Warning() .. "%s|r", currentQuote))
    ApplyFont(footer, footerScheme, "small")
    mainFrame.workflowFooter = footer

    -- House Level display (bottom right) - single line layout
    -- Layout: [XP bar] [XP text] [Level text]
    local houseLevelFrame = CreateFrame("Frame", nil, mainFrame)
    houseLevelFrame:SetSize(320, 20)
    houseLevelFrame:SetPoint("BOTTOMRIGHT", -10, 8)
    mainFrame.houseLevelFrame = houseLevelFrame

    -- XP text (far right)
    local xpScheme = GetScheme()
    local xpText = houseLevelFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    xpText:SetPoint("BOTTOMRIGHT", houseLevelFrame, "BOTTOMRIGHT", 0, 0)
    ApplyFont(xpText, xpScheme, "small")
    if xpScheme then
        xpText:SetTextColor(xpScheme.text_dim.r, xpScheme.text_dim.g, xpScheme.text_dim.b, xpScheme.text_dim.a)
    else
        xpText:SetTextColor(0.6, 0.6, 0.6)
    end
    xpText:SetJustifyH("RIGHT")
    xpText:SetText(HDG.Theme.C_Dim() .. "-- / -- XP|r")  -- Placeholder
    mainFrame.houseXpText = xpText

    -- House level text (to the left of XP text, same baseline)
    local houseLevelText = houseLevelFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    houseLevelText:SetPoint("BOTTOMRIGHT", xpText, "BOTTOMLEFT", -12, 0)
    houseLevelText:SetJustifyH("RIGHT")
    houseLevelText:SetText(HDG.Theme.C_Accent() .. L["UI_HOUSE_LEVEL"] .. "|r " .. HDG.Theme.C_Dim() .. "--|r")  -- Placeholder
    ApplyFont(houseLevelText, xpScheme, "small")
    mainFrame.houseLevelText = houseLevelText

    -- Tooltip on hover
    houseLevelFrame:EnableMouse(true)
    houseLevelFrame:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, "ANCHOR_TOP")
        GameTooltip:SetText(HDG.Theme.C_Accent() .. L["UI_HOUSE_LEVEL"] .. "|r", 1, 1, 1)
        GameTooltip:AddLine(L["TIP_HOUSE_XP_1"], 0.7, 0.7, 0.7, true)
        GameTooltip:AddLine(" ")
        GameTooltip:AddLine(L["TIP_HOUSE_XP_2"], 0.7, 0.7, 0.7, true)
        GameTooltip:Show()
    end)
    houseLevelFrame:SetScript("OnLeave", GameTooltip_Hide)

    -- Initial update
    HDG_UI.UpdateHouseLevelDisplay()

    privateDB.mainFrame = mainFrame
end

-- CreateRecipePanel, CreateQueuePanel, CreateMaterialsPanel moved to HDG_RecipesTab.lua

function HDG_UI.CreateTrainersPanel()
    local trainersFrame = CreateFrame("Frame", nil, mainFrame, "BackdropTemplate")
    trainersFrame:SetPoint("TOPLEFT", CONFIG.SPACING.PANEL_PADDING, -(CONFIG.BANNER_HEIGHT + CONFIG.TAB_HEIGHT + 8))
    trainersFrame:SetSize(CONFIG.MAIN_FRAME_WIDTH - (CONFIG.SPACING.PANEL_PADDING * 2), CONFIG.PANEL_HEIGHT)
    trainersFrame:SetBackdrop({
        bgFile = CONFIG.TEXTURES.WHITE_BG,
        edgeFile = CONFIG.TEXTURES.WHITE_BG,
        tile = false,
        edgeSize = 1,
        insets = { left = 0, right = 0, top = 0, bottom = 0 }
    })
    local trainersScheme = GetScheme()
    trainersFrame:SetBackdropColor(trainersScheme.panel.r, trainersScheme.panel.g, trainersScheme.panel.b, 0.6)
    trainersFrame:SetBackdropBorderColor(trainersScheme.border.r, trainersScheme.border.g, trainersScheme.border.b, trainersScheme.border.a)
    RegisterWidget(trainersFrame, "Panel")
    trainersFrame:Hide()
    mainFrame.trainersFrame = trainersFrame

    -- Trainers implementation details handled by HDG_Trainers module
end

-- CreateCharactersPanel and RefreshCharactersPanel moved to HDG_AltsTab.lua

function HDG_UI.RefreshCharactersPanel()
    -- Delegate to HDG_AltsTab module
    if privateDB.HDG_AltsTab and privateDB.HDG_AltsTab.Refresh then
        privateDB.HDG_AltsTab.Refresh()
    end
end

-- CreateConfigPanel moved to HDG_ConfigTab.lua (stub for compatibility)

function HDG_UI.UpdateTabs()
    local tabKey = GetUIState("activeTab") or "recipes"
    local HDG_Debug = privateDB.HDG_Debug
    local Timed = (HDG_Debug and HDG_Debug.Timed) or function(_, fn) return fn() end

    Timed("UpdateTabs.total[" .. tabKey .. "]", function()

    -- Hide all tabs via module methods
    if HDG_RecipesTab then HDG_RecipesTab:Hide() end
    if HDG_AcquisitionTab then HDG_AcquisitionTab:Hide() end
    if HDG_DecorPreview then HDG_DecorPreview:Hide() end
    if HDG_Trainers then HDG_Trainers:Hide() end
    if privateDB.HDG_AltsTab then privateDB.HDG_AltsTab.Hide() end
    if privateDB.HDG_ConfigTab then privateDB.HDG_ConfigTab.Hide() end
    if privateDB.HDG_DataTab then privateDB.HDG_DataTab.Hide() end
    if privateDB.HDG_GoblinTab then privateDB.HDG_GoblinTab.Hide() end
    if privateDB.HDG_MogulTab  then privateDB.HDG_MogulTab.Hide()  end
    if privateDB.HDG_StylesTab then privateDB.HDG_StylesTab:Hide() end
    if privateDB.HDG_HouseTab    then privateDB.HDG_HouseTab.Hide()    end
    if privateDB.HDG_ProjectsTab then privateDB.HDG_ProjectsTab:Hide() end
    mainFrame.trainersFrame:Hide() -- container frame (not module-managed)

    -- Update tab bar wood sign background (HousingTheme only)
    local tabScheme = GetScheme()
    if mainFrame.tabBgTexture then
        if tabScheme and tabScheme.atlas and tabScheme.atlas.tabBarWoodSign then
            mainFrame.tabBgTexture:SetAtlas(tabScheme.atlas.tabBarWoodSign)
            mainFrame.tabBgTexture:Show()
        else
            mainFrame.tabBgTexture:Hide()
        end
    end

    -- Update tab visuals
    for _, tab in ipairs(mainFrame.tabs or {}) do
        local s = tabScheme -- may be nil early; ThemeEngine handles full reskin
        if tab.key == tabKey then
            tab.isActive = true
            if tab._configAtlas then
                tab.icon:SetAtlas("decor-controls-settings-active")
            elseif tab._isAtlasIcon then
                tab.icon:SetAlpha(1.0)
                if tab.text and s then tab.text:SetTextColor(s.text.r, s.text.g, s.text.b, s.text.a) end
            elseif tab._atlasStates then
                tab.icon:SetAtlas(tab.text and tab._atlasStates.down or tab._atlasStates.over)
                if tab.text and s then tab.text:SetTextColor(s.text.r, s.text.g, s.text.b, s.text.a) end
            else
                if s then
                    tab:SetBackdropColor(s.accent.r, s.accent.g, s.accent.b, 0.15)
                    tab:SetBackdropBorderColor(s.accent.r, s.accent.g, s.accent.b, 0.8)
                end
                if tab.icon then tab.icon:SetAlpha(1.0) end
                if tab.text and s then
                    tab.text:SetTextColor(s.text.r, s.text.g, s.text.b, s.text.a)
                end
            end
            if tab.activeBar then tab.activeBar:Show() end
        else
            tab.isActive = false
            if tab._configAtlas then
                tab.icon:SetAtlas("decor-controls-settings-default")
            elseif tab._isAtlasIcon then
                tab.icon:SetAlpha(0.8)
                if tab.text and s then tab.text:SetTextColor(s.text_dim.r, s.text_dim.g, s.text_dim.b, s.text_dim.a) end
            elseif tab._atlasStates then
                tab.icon:SetAtlas(tab._atlasStates.up)
                if tab.text and s then tab.text:SetTextColor(s.text_dim.r, s.text_dim.g, s.text_dim.b, s.text_dim.a) end
            else
                if s then
                    tab:SetBackdropColor(s.bg.r, s.bg.g, s.bg.b, 0.3)
                    tab:SetBackdropBorderColor(s.border.r, s.border.g, s.border.b, 0.5)
                end
                if tab.icon then tab.icon:SetAlpha(0.6) end
                if tab.text and s then
                    tab.text:SetTextColor(s.text_dim.r, s.text_dim.g, s.text_dim.b, s.text_dim.a)
                end
            end
            if tab.activeBar then tab.activeBar:Hide() end
        end
    end

    -- Show selected tab (separately timed so we see init-vs-show cost
    -- distinct from tab-visuals work above)
    Timed("UpdateTabs.show[" .. tabKey .. "]", function()
        if tabKey == "recipes" then
            if HDG_RecipesTab then HDG_RecipesTab:Show() end
        elseif tabKey == "preview" then
            if HDG_DecorPreview then
                if not HDG_DecorPreview.initialized then
                    HDG_DecorPreview:Init(CONFIG, mainFrame, privateDB)
                    HDG_DecorPreview.initialized = true
                end
                HDG_DecorPreview:Show()
            end
        elseif tabKey == "acquisition" then
            if HDG_AcquisitionTab then
                if not HDG_AcquisitionTab.initialized then
                    HDG_AcquisitionTab:Init(CONFIG, mainFrame, privateDB)
                    HDG_AcquisitionTab.initialized = true
                end
                HDG_AcquisitionTab:Show()
            end
        elseif tabKey == "config" then
            if privateDB.HDG_ConfigTab then privateDB.HDG_ConfigTab.Show() end
        elseif tabKey == "trainers" then
            if HDG_Trainers then
                if not HDG_Trainers.initialized then
                    HDG_Trainers:Init(CONFIG, mainFrame, Data().PlayerHasProfession)
                    HDG_Trainers.initialized = true
                end
                HDG_Trainers:Show()
            end
        elseif tabKey == "characters" then
            if privateDB.HDG_AltsTab then privateDB.HDG_AltsTab.Show() end
        elseif tabKey == "data" then
            if privateDB.HDG_DataTab then privateDB.HDG_DataTab.Show() end
        elseif tabKey == "goblin" then
            if privateDB.HDG_GoblinTab then privateDB.HDG_GoblinTab.Show() end
        elseif tabKey == "mogul" then
            if privateDB.HDG_MogulTab then privateDB.HDG_MogulTab.Show() end
        elseif tabKey == "styles" then
            if privateDB.HDG_StylesTab then
                if not privateDB.HDG_StylesTab.initialized then
                    privateDB.HDG_StylesTab:Init(CONFIG, mainFrame, privateDB)
                end
                privateDB.HDG_StylesTab:Show()
            end
        elseif tabKey == "house" then
            if privateDB.HDG_HouseTab then privateDB.HDG_HouseTab.Show() end
        elseif tabKey == "projects" then
            if privateDB.HDG_ProjectsTab then
                if not privateDB.HDG_ProjectsTab._initialized then
                    privateDB.HDG_ProjectsTab:Init(mainFrame)
                    privateDB.HDG_ProjectsTab._initialized = true
                end
                privateDB.HDG_ProjectsTab:Show()
            end
        end
    end)

    end)  -- close UpdateTabs.total Timed
end

-- Switch to a specific tab programmatically (for cross-tab navigation)
function HDG_UI.SwitchTab(tabKey)
    if HDG.UI and HDG.UI.CloseDropdownPopup then HDG.UI.CloseDropdownPopup() end
    Dispatch("SET_ACTIVE_TAB", { tab = tabKey })
    HDG_UI.UpdateTabs()
end

-- Recipe rendering functions moved to HDG_RecipesTab.lua

function HDG_UI.RefreshUI()
    if not mainFrame or not mainFrame:IsShown() then return end

    local activeTab = GetUIState("activeTab")

    -- Only refresh queue+materials when recipes tab is visible
    if activeTab == "recipes" then

        if privateDB.HDG_RecipesTab then
            privateDB.HDG_QueuePanel.Refresh()
            privateDB.HDG_MaterialsPanel.Refresh()
            privateDB.HDG_RecipesTab:RefreshRecipes()
        end
    elseif activeTab == "preview" then
        if privateDB.HDG_DecorPreview then
            privateDB.HDG_DecorPreview:RefreshList()
        end
    elseif activeTab == "trainers" and HDG_Trainers then

        HDG_Trainers:Refresh()
    elseif activeTab == "acquisition" then

        if HDG_AcquisitionTab and HDG_AcquisitionTab.RefreshList then
            HDG_AcquisitionTab:RefreshList()
        end
    else

    end

    HDG_UI.RefreshLumberTracking()
end

-- Update lumber layout based on pin state
-- When NOT pinned: show embedded lumber frame at bottom of queue panel
-- When pinned: hide embedded frame, show separate floating window
function HDG_UI.UpdateLumberLayout()
    if not mainFrame then return end

    local isPinned = GetUIState("lumberPinned")

    -- Handle embedded lumber frame visibility (queue scroll is now a ScrollBox;
    -- old mainFrame.queueScroll field is gone, replaced by mainFrame.queueScrollBox)
    if mainFrame.lumberFrame then
        if isPinned then
            mainFrame.lumberFrame:Hide()
            if mainFrame.queueScrollBox then
                mainFrame.queueScrollBox:SetPoint("BOTTOMRIGHT", -20, 8)
            end
        else
            mainFrame.lumberFrame:Show()
            if mainFrame.queueScrollBox then
                local lumberReservedHeight = 24 + (#HDG_ExpansionData.LUMBER_ORDER * 18) + 8 + 8
                mainFrame.queueScrollBox:SetPoint("BOTTOMRIGHT", -20, lumberReservedHeight)
            end
            HDG_UI.RefreshEmbeddedLumber()
        end
    end

    -- Manage pinned widget visibility
    if isPinned then
        if HDG_LumberTracker and HDG_LumberTracker.ShowPinned then
            HDG_LumberTracker.ShowPinned()
        end
    else
        if HDG_LumberTracker and HDG_LumberTracker.HidePinned then
            HDG_LumberTracker.HidePinned()
        end
    end
end

-- Refresh embedded lumber frame content (used when not pinned)
function HDG_UI.RefreshEmbeddedLumber()
    if not mainFrame or not mainFrame.lumberContent then return end

    -- Reuse cached lumber data from LumberTracker (avoids recalculating)
    local queueLumber, allLumber
    if HDG_LumberTracker and HDG_LumberTracker.GetCachedLumberData then
        queueLumber, allLumber = HDG_LumberTracker.GetCachedLumberData()
    end

    -- Fallback: calculate fresh if cache not yet populated
    if not queueLumber or not allLumber then
        local dataModule = Data()
        if not dataModule.CalculateCraftingQueueLumber then return end
        queueLumber = dataModule.CalculateCraftingQueueLumber(GetCraftingQueue())
        allLumber = dataModule.CalculateLumberNeeds(privateDB.professions or {})
    end

    local expansionOrder = HDG_ExpansionData.LUMBER_ORDER

    -- State snapshot early-exit: skip UI rebuild if nothing changed
    local currentState = {}
    local stateIdx = 0
    for _, lumberId in ipairs(expansionOrder) do
        local sd = queueLumber[lumberId]
        local ad = allLumber[lumberId]
        stateIdx = stateIdx + 1
        currentState[stateIdx] = lumberId
        stateIdx = stateIdx + 1
        currentState[stateIdx] = sd and sd.have or 0
        stateIdx = stateIdx + 1
        currentState[stateIdx] = sd and sd.needed or 0
        stateIdx = stateIdx + 1
        currentState[stateIdx] = ad and ad.have or 0
        stateIdx = stateIdx + 1
        currentState[stateIdx] = ad and ad.needed or 0
    end

    if embeddedLumberState then
        local unchanged = (#currentState == #embeddedLumberState)
        if unchanged then
            for i = 1, #currentState do
                if currentState[i] ~= embeddedLumberState[i] then
                    unchanged = false
                    break
                end
            end
        end
        if unchanged then return end
    end
    embeddedLumberState = currentState

    ClearScrollContent(mainFrame.lumberContent)

    local LUMBER_TYPES = Data().LUMBER_TYPES
    local yOffset = 0
    for _, lumberId in ipairs(expansionOrder) do
        local queueData = queueLumber[lumberId]
        local allData = allLumber[lumberId]
        local lumberInfo = LUMBER_TYPES[lumberId]

        -- Create row using the shared helper (no farming options for embedded panel)
        yOffset = HDG_UIHelpers.CreateLumberRow(mainFrame.lumberContent, lumberId, lumberInfo, queueData, allData, yOffset, nil)
    end
end

function HDG_UI.RefreshLumberTracking()
    if not mainFrame then return end

    -- Update lumber button text (in recipes tab) if it exists
    if mainFrame.lumberBtn and mainFrame.lumberBtn.UpdateText then
        mainFrame.lumberBtn.UpdateText()
    end

    -- Manage embedded vs pinned lumber display
    local isPinned = GetUIState("lumberPinned")

    -- Refresh embedded lumber content if visible (not pinned)
    if not isPinned and mainFrame.lumberFrame and mainFrame.lumberFrame:IsShown() then
        HDG_UI.RefreshEmbeddedLumber()
    end

    -- Refresh the independent pinned widget if visible
    if HDG_LumberTracker and HDG_LumberTracker.IsPinnedVisible and HDG_LumberTracker.IsPinnedVisible() then
        HDG_LumberTracker.RefreshPinned()
    end
end

-- ============================================================================
-- DELEGATION TO HDG_RecipesTab
-- These functions delegate to HDG_RecipesTab for the actual implementation
-- ============================================================================

function HDG_UI.RefreshProfessionTabs()
    if privateDB.HDG_ProfessionSidebar then
        privateDB.HDG_ProfessionSidebar.Refresh()
    end
end

function HDG_UI.AddToCraftingQueue(source, skipRefresh, quantity)
    if privateDB.HDG_RecipesTab then
        privateDB.HDG_RecipesTab:AddToCraftingQueue(source, skipRefresh, quantity)
    end
end

function HDG_UI.UpdateShopListBadge()
    if not mainFrame or not mainFrame.shopListBtn then return end
    local count = HDG_VendorShoppingList and HDG_VendorShoppingList.GetCount() or 0
    local slLabel = L["HEADER_SHOPPING_LIST"] or "Shopping List"
    local text = count > 0 and (slLabel .. " (" .. count .. ")") or slLabel
    mainFrame.shopListBtn.slText:SetText(text)
    local textWidth = mainFrame.shopListBtn.slText:GetStringWidth()
    mainFrame.shopListBtn:SetWidth(math.max(140, textWidth + 30)) -- icon(14) + padding(8+4+8)
end

function HDG_UI.RefreshCraftingQueue()
    if privateDB.HDG_QueuePanel then
        privateDB.HDG_QueuePanel.Refresh()
    end
end

function HDG_UI.RefreshMaterials()
    if privateDB.HDG_MaterialsPanel then
        privateDB.HDG_MaterialsPanel.Refresh()
    end
end

function HDG_UI.AddAllMaterialsToAuctionator()
    if privateDB.HDG_MaterialsPanel then
        privateDB.HDG_MaterialsPanel.AddAllToAuctionator()
    end
end

-- Debug timer for cold-login measurement: window-open -> HDG_COLLECTION_READY
local _refreshStartTime = nil
local _refreshTimerListenerRegistered = false

-- Refresh everything: clear non-collection caches, force a fresh owned-items
-- search, re-scan profession skills, and refresh every tab. Called by both the
-- Refresh button (with toast) and the main window OnShow (silent), so cold
-- login follows the same path the Refresh button has always followed -- the
-- catalog is reliably ready by user-action time.
function HDG_UI.RefreshAll(silent)
    -- Timer trace: window-open -> HDG_COLLECTION_READY. Gated behind
    -- /hdgdebug timer (separate from general debug mode; default off).
    local timerTrace = privateDB.HDG_Debug and privateDB.HDG_Debug.IsTimerDebug and privateDB.HDG_Debug.IsTimerDebug()
    if timerTrace then
        _refreshStartTime = GetTime()
        print(string.format("|cFFFFFF00[HDG Timer]|r RefreshAll started (silent=%s)", tostring(silent)))
        if not _refreshTimerListenerRegistered and HDG.EventBus then
            _refreshTimerListenerRegistered = true
            HDG.EventBus:Register("HDG_COLLECTION_READY", function()
                if _refreshStartTime then
                    local elapsed = GetTime() - _refreshStartTime
                    print(string.format("|cFFFFFF00[HDG Timer]|r Collection populated in %.3fs", elapsed))
                    _refreshStartTime = nil
                end
            end)
        end
    end

    Data().ClearCaches()
    -- Collection cache is kept current by HOUSING_STORAGE_UPDATED /
    -- HOUSING_STORAGE_ENTRY_UPDATED events in HDG_Data — owned state doesn't
    -- change except via placement/acquire/destroy, all of which fire those
    -- events. Only invalidate + re-search on an explicit refresh click.
    -- On window open (silent=true), warm the cache if this is the first use
    -- but otherwise keep whatever we already have.
    if silent then
        if HDG_Data.WarmCollectionCache then HDG_Data.WarmCollectionCache() end
    else
        if HDG_Data.InvalidateCollectionCache then HDG_Data.InvalidateCollectionCache() end
        -- Refresh button: also reset Preview's independent searcher state so
        -- a stuck "in progress" flag doesn't block TriggerAllDecorSearch from
        -- running again. Preview owns its own searcher + cache (separate from
        -- HDG_Data), so invalidation has to be explicit.
        if HDG_DecorPreview and HDG_DecorPreview.InvalidateCache then
            HDG_DecorPreview:InvalidateCache()
        end
    end
    HDG_Data.CollectCharacterProfessions()

    if privateDB.HDG_RecipesTab then privateDB.HDG_RecipesTab:Refresh() end
    if privateDB.HDG_StylesTab and privateDB.HDG_StylesTab.Refresh then privateDB.HDG_StylesTab:Refresh() end
    if privateDB.HDG_GoblinTab and privateDB.HDG_GoblinTab.Refresh then privateDB.HDG_GoblinTab.Refresh() end
    if HDG_DecorPreview and HDG_DecorPreview.Refresh then HDG_DecorPreview:Refresh() end
    if _G.HDG_Trainers and _G.HDG_Trainers.Refresh then _G.HDG_Trainers:Refresh() end
    if privateDB.HDG_AltsTab and privateDB.HDG_AltsTab.Refresh then privateDB.HDG_AltsTab.Refresh() end
    if privateDB.HDG_DataTab and privateDB.HDG_DataTab.RefreshFarmingDisplay then privateDB.HDG_DataTab.RefreshFarmingDisplay() end

    HDG_UI.RefreshMaterials()
    HDG_UI.RefreshLumberTracking()

    if not silent then
        print(HDG.Theme.C_Success() .. "Housing Decor Guide:|r Refreshed all tabs and cleared caches")
    end
end

-- Toggle UI
function HDG_UI.Toggle()
    -- Combat safety: defer UI toggle until out of combat
    if InCombatLockdown() then
        print(HDG.Theme.C_Warning() .. "[Housing Decor Guide]|r Cannot toggle window during combat.")
        return
    end

    if not mainFrame then HDG_UI.CreateUI() end
    if mainFrame:IsShown() then
        -- If lumber is pinned, show the independent pinned widget when closing main window
        if GetUIState("lumberPinned") then
            HDG_LumberTracker.ShowPinned()
        end
        mainFrame:Hide()
    else
        mainFrame:Show()
        HDG_UI.RefreshUI()
    end
end

-- Open HDG with a specific profession filter (v1.9.5)
function HDG_UI.OpenWithProfession(professionName)
    -- Combat safety
    if InCombatLockdown() then
        print(HDG.Theme.C_Warning() .. "[Housing Decor Guide]|r Cannot open window during combat.")
        return
    end

    if not mainFrame then HDG_UI.CreateUI() end

    -- Set the profession filter (per-character)
    if professionName then
        HDG.Store:SetCharacterProfessionFilter(professionName)
    end

    -- Ensure we're on the recipes tab
    Dispatch("SET_ACTIVE_TAB", { tab = "recipes" })

    mainFrame:Show()
    HDG_UI.RefreshUI()
end

-- ============================================================================
-- PROFESSION WINDOW INTEGRATION (v1.9.5, v2.0.5)
-- Adds "Decor Guide" and "Filter Decor" buttons to Blizzard's profession UI
-- ============================================================================

local professionButton = nil
local filterDecorButton = nil
local filterDecorActive = false  -- Track filter toggle state

-- Use centralized profession data from HDG_ExpansionData
local function GetCurrentProfessionName()
    if not C_TradeSkillUI.IsTradeSkillReady() then return nil end

    local professionInfo = C_TradeSkillUI.GetBaseProfessionInfo()
    if professionInfo and professionInfo.professionID then
        return HDG_ExpansionData.GetProfessionName(professionInfo.professionID)
    end
    return nil
end

-- Helper to create a teal-styled button
local function CreateTealButton(name, parent, width, text)
    local btn = CreateFrame("Button", name, parent, "BackdropTemplate")
    btn:SetSize(width, 22)
    btn:SetFrameStrata("HIGH")
    btn:SetFrameLevel(500)

    btn:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8x8",
        edgeFile = "Interface\\Buttons\\WHITE8x8",
        tile = false,
        edgeSize = 1,
        insets = { left = 0, right = 0, top = 0, bottom = 0 }
    })
    local tealScheme = GetScheme()
    if tealScheme then
        btn:SetBackdropColor(tealScheme.accent.r, tealScheme.accent.g, tealScheme.accent.b, 0.9)
        btn:SetBackdropBorderColor(tealScheme.accent.r, tealScheme.accent.g, tealScheme.accent.b, tealScheme.accent.a)
    else
        btn:SetBackdropColor(0.08, 0.45, 0.42, 0.9)  -- Teal background
        btn:SetBackdropBorderColor(0.1, 0.6, 0.55, 1)  -- Lighter teal border
    end

    local btnText = btn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    btnText:SetPoint("CENTER", 0, 0)
    ApplyFont(btnText, tealScheme, "small")
    btnText:SetText(HDG.Theme.C_Text() .. text .. "|r")
    btn.text = btnText

    -- Mark as accent button and register for theme updates
    btn.isAccentButton = true
    RegisterWidget(btn, "AccentButton")

    return btn
end

local function CreateProfessionButtons()
    if professionButton then return end

    -- Check if buttons are enabled in settings
    if GetConfig("showProfessionButtons") == false then
        return
    end

    -- ProfessionsFrame is the retail profession UI
    local parentFrame = ProfessionsFrame
    if not parentFrame then return end

    -- Create "Decor Guide" button (opens HDG filtered to current profession)
    professionButton = CreateTealButton("HDG_ProfessionButton", parentFrame, 90, L["BTN_DECOR_GUIDE"] or "Decor Guide")
    professionButton:SetPoint("TOPRIGHT", parentFrame, "TOPRIGHT", -110, -1)

    professionButton:SetScript("OnClick", function()
        local profName = GetCurrentProfessionName()
        if profName and HDG_ExpansionData.GetProfessionID(profName) then
            HDG_UI.OpenWithProfession(profName)
        else
            HDG_UI.Toggle()
        end
    end)

    professionButton:SetScript("OnEnter", function(self)
        local hoverScheme = GetScheme()
        if hoverScheme then
            self:SetBackdropColor(hoverScheme.button_hover.r, hoverScheme.button_hover.g, hoverScheme.button_hover.b, hoverScheme.button_hover.a)
        else
            self:SetBackdropColor(0.1, 0.55, 0.5, 1)
        end
        GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
        local profName = GetCurrentProfessionName()
        if profName and HDG_ExpansionData.GetProfessionID(profName) then
            GameTooltip:AddLine(HDG.Theme.C_Warning() .. L["TIP_OPEN_DECOR_GUIDE"] .. "|r")
            GameTooltip:AddLine(string.format(L["TIP_FILTERED_TO"], profName), 0.7, 0.7, 0.7)
        else
            GameTooltip:AddLine(HDG.Theme.C_Warning() .. L["TIP_OPEN_DECOR_GUIDE"] .. "|r")
            GameTooltip:AddLine(L["TIP_VIEW_ALL_DECOR"], 0.7, 0.7, 0.7)
        end
        GameTooltip:Show()
    end)

    professionButton:SetScript("OnLeave", function(self)
        local leaveScheme = GetScheme()
        if leaveScheme then
            self:SetBackdropColor(leaveScheme.accent.r, leaveScheme.accent.g, leaveScheme.accent.b, 0.9)
        else
            self:SetBackdropColor(0.08, 0.45, 0.42, 0.9)
        end
        GameTooltip:Hide()
    end)

    -- Create "Filter Decor" button (filters profession window to House Decor)
    local filterDecorLabel = L["BTN_FILTER_DECOR"] or "Filter Decor"
    filterDecorButton = CreateTealButton("HDG_FilterDecorButton", parentFrame, 85, filterDecorLabel)
    filterDecorButton:SetPoint("RIGHT", professionButton, "LEFT", -4, 0)

    -- Helper to update filter button appearance based on state
    local function UpdateFilterButtonAppearance()
        local filterScheme = GetScheme()
        if filterDecorActive then
            -- Active state: brighter teal with warning border
            if filterScheme then
                filterDecorButton:SetBackdropColor(filterScheme.button_hover.r, filterScheme.button_hover.g, filterScheme.button_hover.b, filterScheme.button_hover.a)
                filterDecorButton:SetBackdropBorderColor(filterScheme.warning.r, filterScheme.warning.g, filterScheme.warning.b, filterScheme.warning.a)
            else
                filterDecorButton:SetBackdropColor(0.1, 0.55, 0.5, 1)
                filterDecorButton:SetBackdropBorderColor(0.99, 0.83, 0.22, 1)  -- Gold border when active
            end
            filterDecorButton.text:SetText(HDG.Theme.C_Warning() .. filterDecorLabel .. "|r")  -- Active
        else
            -- Inactive state: normal accent color
            if filterScheme then
                filterDecorButton:SetBackdropColor(filterScheme.accent.r, filterScheme.accent.g, filterScheme.accent.b, 0.9)
                filterDecorButton:SetBackdropBorderColor(filterScheme.accent.r, filterScheme.accent.g, filterScheme.accent.b, filterScheme.accent.a)
            else
                filterDecorButton:SetBackdropColor(0.08, 0.45, 0.42, 0.9)
                filterDecorButton:SetBackdropBorderColor(0.1, 0.6, 0.55, 1)
            end
            filterDecorButton.text:SetText(HDG.Theme.C_Text() .. filterDecorLabel .. "|r")  -- Inactive
        end
    end

    filterDecorButton:SetScript("OnClick", function()
        if C_TradeSkillUI and C_TradeSkillUI.SetRecipeItemNameFilter then
            if filterDecorActive then
                -- Clear the filter
                C_TradeSkillUI.SetRecipeItemNameFilter("")
                filterDecorActive = false
            else
                -- Apply the filter
                C_TradeSkillUI.SetRecipeItemNameFilter("House Decor")
                filterDecorActive = true
            end
            UpdateFilterButtonAppearance()
        end
    end)

    filterDecorButton:SetScript("OnEnter", function(self)
        if not filterDecorActive then
            local enterScheme = GetScheme()
            if enterScheme then
                self:SetBackdropColor(enterScheme.button_hover.r, enterScheme.button_hover.g, enterScheme.button_hover.b, enterScheme.button_hover.a)
            else
                self:SetBackdropColor(0.1, 0.55, 0.5, 1)
            end
        end
        GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
        if filterDecorActive then
            GameTooltip:AddLine(HDG.Theme.C_Warning() .. L["TIP_CLEAR_FILTER"] .. "|r")
            GameTooltip:AddLine(L["TIP_CLICK_SHOW_ALL"], 1, 1, 1, true)
        else
            GameTooltip:AddLine(HDG.Theme.C_Warning() .. L["TIP_FILTER_HOUSE_DECOR"] .. "|r")
            GameTooltip:AddLine(L["TIP_FILTER_HOUSE_DECOR_DESC"], 1, 1, 1, true)
            GameTooltip:AddLine(" ")
            GameTooltip:AddLine(HDG.Theme.C_Dim() .. L["TIP_FILTER_NOTE_1"] .. "|r", 0.7, 0.7, 0.7, true)
            GameTooltip:AddLine(HDG.Theme.C_Dim() .. L["TIP_FILTER_NOTE_2"] .. "|r", 0.7, 0.7, 0.7, true)
        end
        GameTooltip:Show()
    end)

    filterDecorButton:SetScript("OnLeave", function(self)
        UpdateFilterButtonAppearance()
        GameTooltip:Hide()
    end)

    -- Hook into profession frame show/update for visibility
    parentFrame:HookScript("OnShow", function()
        -- Reset filter state when window opens (filter text is cleared by WoW)
        filterDecorActive = false
        UpdateFilterButtonAppearance()

        if GetConfig("showProfessionButtons") == false then
            if professionButton then professionButton:Hide() end
            if filterDecorButton then filterDecorButton:Hide() end
        else
            if professionButton then professionButton:Show() end
            if filterDecorButton then filterDecorButton:Show() end
        end
    end)
end

-- Function to update profession button visibility based on settings
function HDG_UI.UpdateProfessionButtonVisibility()
    if GetConfig("showProfessionButtons") == false then
        if professionButton then professionButton:Hide() end
        if filterDecorButton then filterDecorButton:Hide() end
    else
        if professionButton then professionButton:Show() end
        if filterDecorButton then filterDecorButton:Show() end
    end
end

-- Hook into profession frame loading
local professionHookFrame = CreateFrame("Frame")
professionHookFrame:RegisterEvent("ADDON_LOADED")
professionHookFrame:RegisterEvent("TRADE_SKILL_SHOW")
professionHookFrame:SetScript("OnEvent", function(self, event, arg1)
    if event == "ADDON_LOADED" and arg1 == "Blizzard_Professions" then
        -- Blizzard profession UI loaded, create our buttons
        C_Timer.After(0.1, CreateProfessionButtons)
    elseif event == "TRADE_SKILL_SHOW" then
        -- Profession window opened, ensure buttons exist
        if ProfessionsFrame then
            CreateProfessionButtons()
        end
    end
end)

-- Function to refresh the random quote
function HDG_UI.RefreshQuote()
    currentQuote = ORC_QUOTES[math.random(#ORC_QUOTES)]
    if mainFrame and mainFrame.workflowFooter then
        mainFrame.workflowFooter:SetText(string.format(HDG.Theme.C_Accent() .. "Vamoose says:|r " .. HDG.Theme.C_Warning() .. "%s|r", currentQuote))
    end
end

-- Cache for house info
local cachedHouseList = nil
local cachedHouseGUID = nil
local cachedHouseLevel = nil
local cachedHouseXP = nil

-- Request house list (async - will fire PLAYER_HOUSE_LIST_UPDATED)
function HDG_UI.RequestHouseInfo()
    if C_Housing and C_Housing.GetPlayerOwnedHouses then
        pcall(C_Housing.GetPlayerOwnedHouses)
    end
end

-- Called when PLAYER_HOUSE_LIST_UPDATED fires
function HDG_UI.OnHouseListUpdated(houseInfoList)
    cachedHouseList = houseInfoList
    if houseInfoList and #houseInfoList > 0 then
        cachedHouseGUID = houseInfoList[1].houseGUID
        -- Now request the favor info (async - will fire HOUSE_LEVEL_FAVOR_UPDATED)
        if cachedHouseGUID and C_Housing and C_Housing.GetCurrentHouseLevelFavor then
            pcall(C_Housing.GetCurrentHouseLevelFavor, cachedHouseGUID)
        end
    else
        -- No houses - hide the display
        if mainFrame and mainFrame.houseLevelFrame then
            mainFrame.houseLevelFrame:Hide()
        end
        if mainFrame and mainFrame.houseLevelRing then
            mainFrame.houseLevelRing:Hide()
        end
    end
end

-- Called when HOUSE_LEVEL_FAVOR_UPDATED fires with favor data
function HDG_UI.OnHouseLevelFavorUpdated(houseLevelFavor)
    if not mainFrame or not mainFrame.houseLevelText then return end
    if not houseLevelFavor then
        mainFrame.houseLevelFrame:Hide()
        if mainFrame.houseLevelRing then mainFrame.houseLevelRing:Hide() end
        return
    end

    -- From Blizzard code: houseLevelFavor has houseGUID, houseLevel, houseFavor
    local currentLevel = houseLevelFavor.houseLevel or 1
    local currentXP = houseLevelFavor.houseFavor or 0
    cachedHouseLevel = currentLevel
    cachedHouseXP = currentXP

    -- Get max level
    local maxLevel = 50
    if C_Housing and C_Housing.GetMaxHouseLevel then
        local success, max = pcall(C_Housing.GetMaxHouseLevel)
        if success and max then maxLevel = max end
    end

    -- Get XP needed for next level (GetHouseLevelFavorForLevel returns a number directly)
    local xpForNextLevel = 0
    if currentLevel < maxLevel and C_Housing and C_Housing.GetHouseLevelFavorForLevel then
        local success, needed = pcall(C_Housing.GetHouseLevelFavorForLevel, currentLevel + 1)
        if success and needed then
            xpForNextLevel = needed
        end
    end

    -- Update level text
    if currentLevel >= maxLevel then
        mainFrame.houseLevelText:SetText(string.format(HDG.Theme.C_Accent() .. "House Level|r " .. HDG.Theme.C_Warning() .. "%d|r " .. HDG.Theme.C_Dim() .. "(Max)|r", currentLevel))
    else
        mainFrame.houseLevelText:SetText(string.format(HDG.Theme.C_Accent() .. "House Level|r " .. HDG.Theme.C_Warning() .. "%d|r", currentLevel))
    end

    -- Calculate XP progress for THIS level (not total)
    -- Need XP from previous level to current level
    local xpForCurrentLevel = 0
    if currentLevel > 0 and C_Housing and C_Housing.GetHouseLevelFavorForLevel then
        local success, needed = pcall(C_Housing.GetHouseLevelFavorForLevel, currentLevel)
        if success and needed then
            xpForCurrentLevel = needed
        end
    end

    -- Update XP text (always show current / threshold XP)
    -- At max level, xpForNextLevel will be 0, so use xpForCurrentLevel as the threshold
    local xpThreshold = xpForNextLevel > 0 and xpForNextLevel or xpForCurrentLevel
    mainFrame.houseXpText:SetText(string.format("%d / %d XP", currentXP, xpThreshold))

    mainFrame.houseLevelFrame:Show()

    -- Update ring widget
    if mainFrame.houseLevelRing then
        local ring = mainFrame.houseLevelRing
        ring.levelText:SetText(currentLevel)
        if currentLevel >= maxLevel then
            ring.tooltipText = string.format("House Level %d (Max)", currentLevel)
        else
            local xpIntoLevel = currentXP - xpForCurrentLevel
            local xpNeededForLevel = xpForNextLevel - xpForCurrentLevel
            ring.tooltipText = string.format("House Level %d -- %d / %d XP", currentLevel, xpIntoLevel, xpNeededForLevel)
        end
        ring:Show()
    end
end

-- Expose cached house data for HouseTab
function HDG_UI.GetCachedHouseData()
    if not C_Housing or not cachedHouseLevel then return nil end

    local maxLevel = 50
    if C_Housing.GetMaxHouseLevel then
        local ok, ml = pcall(C_Housing.GetMaxHouseLevel)
        if ok and ml then maxLevel = ml end
    end

    -- Build cumulative XP thresholds for each level. C_Housing.GetHouseLevelFavorForLevel
    -- returns the cumulative XP needed to BE at that level (level 1 = 0). We
    -- carry a fallback table for environments where the API returns nil/0.
    local FALLBACK_HOUSE_LEVEL_XP = {
        [1] = 0,    [2] = 250,   [3] = 750,   [4] = 1500,  [5] = 2500,
        [6] = 4000, [7] = 6000,  [8] = 9000,  [9] = 12900,
    }
    local thresholds = {}
    if C_Housing.GetHouseLevelFavorForLevel then
        for i = 1, maxLevel do
            local ok, val = pcall(C_Housing.GetHouseLevelFavorForLevel, i)
            thresholds[i] = (ok and val and val > 0) and val or FALLBACK_HOUSE_LEVEL_XP[i] or 0
        end
    else
        for i = 1, maxLevel do
            thresholds[i] = FALLBACK_HOUSE_LEVEL_XP[i] or 0
        end
    end

    return { level = cachedHouseLevel, xp = cachedHouseXP or 0, maxLevel = maxLevel, thresholds = thresholds }
end

-- Legacy function for compatibility - just requests the data
function HDG_UI.UpdateHouseLevelDisplay()
    if not mainFrame or not mainFrame.houseLevelText then return end

    -- Check if C_Housing API is available
    if not C_Housing then
        if mainFrame.houseLevelFrame then
            mainFrame.houseLevelFrame:Hide()
        end
        if mainFrame.houseLevelRing then
            mainFrame.houseLevelRing:Hide()
        end
        return
    end

    -- Request house info (async)
    HDG_UI.RequestHouseInfo()
end

-- Register for PLAYER_ENTERING_WORLD to change quote and house level events
local quoteFrame = CreateFrame("Frame")
quoteFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
quoteFrame:RegisterEvent("PLAYER_HOUSE_LIST_UPDATED")
quoteFrame:RegisterEvent("HOUSE_LEVEL_FAVOR_UPDATED")
quoteFrame:RegisterEvent("HOUSE_LEVEL_CHANGED")
quoteFrame:SetScript("OnEvent", function(self, event, ...)
    if event == "PLAYER_ENTERING_WORLD" then
        HDG_UI.RefreshQuote()
        HDG_UI.UpdateHouseLevelDisplay()
    elseif event == "PLAYER_HOUSE_LIST_UPDATED" then
        -- Receives houseInfoList as first argument
        local houseInfoList = ...
        HDG_UI.OnHouseListUpdated(houseInfoList)
    elseif event == "HOUSE_LEVEL_FAVOR_UPDATED" then
        -- Receives houseLevelFavor as first argument
        local houseLevelFavor = ...
        HDG_UI.OnHouseLevelFavorUpdated(houseLevelFavor)
    elseif event == "HOUSE_LEVEL_CHANGED" then
        -- Level changed - request fresh data
        HDG_UI.UpdateHouseLevelDisplay()
    end
end)

-- ============================================================================
-- THROTTLED REFRESH FAN-OUTS
-- Cross-cutting refresh helpers that touch many UI surfaces in one debounced
-- pass. Exposed via privateDB so other modules (HDG_LumberTracker on bank/
-- mail/merchant close, HDG_Data on TRADE_SKILL_LIST_UPDATE, main file on
-- BAG_UPDATE_DELAYED) can hook into the same throttle instead of each
-- re-implementing it.
-- ============================================================================

-- 0.3s debounce. Fires once per burst of inventory-changing events (bag,
-- mail, merchant, bank, money). Saves char-lumber inventory, runs the
-- LumberTracker change check, and refreshes whichever surfaces are visible.
local _inventoryUpdatePending = false
function HDG_UI.QueueInventoryUpdate()
    if _inventoryUpdatePending then return end
    _inventoryUpdatePending = true
    C_Timer.After(0.3, function()
        _inventoryUpdatePending = false
        if HDG_Data and HDG_Data.SaveCharacterLumberInventory then
            HDG_Data.SaveCharacterLumberInventory()
        end
        local lumberChanged = HDG_LumberTracker
            and HDG_LumberTracker.CheckForLumberChanges
            and HDG_LumberTracker.CheckForLumberChanges()

        local mainWindowVisible = privateDB.mainFrame and privateDB.mainFrame:IsShown()
        local pinnedLumberVisible = HDG_LumberTracker
            and HDG_LumberTracker.IsPinnedVisible
            and HDG_LumberTracker.IsPinnedVisible()

        if mainWindowVisible then
            HDG_UI.RefreshCraftingQueue()
            HDG_UI.RefreshMaterials()
            HDG_UI.RefreshLumberTracking()
            if privateDB.Log then
                privateDB.Log(privateDB.LOG_LEVEL.DEBUG, "Inventory updated - materials refreshed")
            end
        elseif pinnedLumberVisible then
            HDG_LumberTracker.RefreshPinned()
            if privateDB.Log then
                privateDB.Log(privateDB.LOG_LEVEL.DEBUG,
                    "Inventory updated - pinned lumber refreshed"
                    .. (lumberChanged and " (lumber collected)" or ""))
            end
        end

        if HDG_MaterialsList and HDG_MaterialsList.IsShown
            and HDG_MaterialsList.IsShown() then
            HDG_MaterialsList.Refresh()
        end
    end)
end
privateDB.QueueInventoryUpdate = HDG_UI.QueueInventoryUpdate

-- 0.5s debounce. Fires once per burst of TRADE_SKILL_LIST_UPDATE /
-- NEW_RECIPE_LEARNED. Refreshes the main frame if visible -- recipe
-- additions invalidate caches in HDG_Data and need a UI repaint.
local _recipeUpdatePending = false
function HDG_UI.QueueRecipeUpdate()
    if _recipeUpdatePending then return end
    _recipeUpdatePending = true
    C_Timer.After(0.5, function()
        _recipeUpdatePending = false
        if privateDB.mainFrame and privateDB.mainFrame:IsShown() then
            HDG_UI.RefreshUI()
            if privateDB.Log then
                privateDB.Log(privateDB.LOG_LEVEL.DEBUG, "Recipe learned - UI refreshed")
            end
        end
    end)
end
privateDB.QueueRecipeUpdate = HDG_UI.QueueRecipeUpdate

-- ============================================================================
-- BLIZZARD EVENT WIRING
-- HOUSING_STORAGE_ENTRY_UPDATED fires when Blizzard's catalog cache lands a
-- per-entry update. Refresh the main frame if it's visible so storage counts
-- and collection badges repaint immediately. The full collection cache stays
-- coherent via the central HDG_Data sweep + HDG_COLLECTION_READY event; this
-- handler is just the UI repaint hook.
-- ============================================================================
function HDG_UI.RegisterEvents()
    local f = CreateFrame("Frame")
    f:RegisterEvent("HOUSING_STORAGE_ENTRY_UPDATED")
    f:SetScript("OnEvent", function(_, event)
        if event == "HOUSING_STORAGE_ENTRY_UPDATED" then
            if privateDB.mainFrame and privateDB.mainFrame:IsShown() then
                HDG_UI.RefreshUI()
            end
        end
    end)
end

-- Export UI module
_G.HDG_UI = HDG_UI
privateDB.RefreshUI = HDG_UI.RefreshUI
