local addonName, privateDB = ...

-- ============================================================================
-- ALTS TAB MODULE
-- Displays character profession data across all alts
-- Extracted from HDG_UI.lua in v2.0.3 for maintainability
-- ============================================================================

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

local mainFrame = nil
local altsDirty = false -- deferred refresh when tab is hidden
local HDG = _G.HousingDecorGuide
local function GetScheme() return HDG.Theme.GetCurrentScheme() end

-- ============================================================================
-- ROW POOLS (avoid destroying/recreating frames on every Refresh)
-- ============================================================================

local EXPANSION_SLOTS = 12

local noProfCollapsed = true
local showHiddenChars = false

local function IsHidden(charKey)
    local state = HDG.Store:GetState()
    return state and state.ui and state.ui.hiddenCharacters and state.ui.hiddenCharacters[charKey]
end

local function SetHidden(charKey, hidden)
    local state = HDG.Store:GetState()
    if not state or not state.ui then return end
    if not state.ui.hiddenCharacters then state.ui.hiddenCharacters = {} end
    state.ui.hiddenCharacters[charKey] = hidden or nil
    HDG.Store:QueueSave()
    HDG_AltsTab.Refresh()
end

local function DeleteCharacter(charKey)
    local state = HDG.Store:GetState()
    if state and state.characters then state.characters[charKey] = nil end
    if state and state.ui then
        if state.ui.hiddenCharacters then state.ui.hiddenCharacters[charKey] = nil end
        if state.ui.characterProfessionFilter then state.ui.characterProfessionFilter[charKey] = nil end
    end
    HDG.Store:QueueSave()
    HDG_AltsTab.Refresh()
end

StaticPopupDialogs["HDG_CONFIRM_DELETE_CHAR"] = {
    text = "Delete all saved data for %s?\n\nThis removes professions, lumber, and recipe data for this character and cannot be undone.",
    button1 = "Delete",
    button2 = "Cancel",
    OnAccept = function(_, data)
        if data then DeleteCharacter(data) end
    end,
    timeout = 0,
    whileDead = true,
    hideOnEscape = true,
}

local profRowPool = HDG_UIHelpers.CreateRowPool(function(parent)
    local row = CreateFrame("Frame", nil, parent, "BackdropTemplate")
    row:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8" })

    row.profLabel = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    row.profLabel:SetPoint("LEFT", 8, 0)
    row.profLabel:SetJustifyH("LEFT")

    row.skillLabels = {}
    row.skillBtns = {}
    row.recipePips = {}
    for i = 1, EXPANSION_SLOTS do
        local btn = CreateFrame("Button", nil, row)
        btn:SetSize(60, 18)
        local label = btn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        label:SetPoint("CENTER", 0, 0)
        label:SetJustifyH("CENTER")
        row.skillBtns[i] = btn
        row.skillLabels[i] = label
        local pip = btn:CreateTexture(nil, "OVERLAY")
        pip:SetSize(8, 8)
        pip:Hide()
        row.recipePips[i] = pip
    end

    row.scanLabel = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    row.scanLabel:SetPoint("LEFT", 108, 0)
    row.scanLabel:SetJustifyH("LEFT")
    row.scanLabel:Hide()
    return row
end)

local headerRowPool = HDG_UIHelpers.CreateRowPool(function(parent)
    local row = CreateFrame("Frame", nil, parent, "BackdropTemplate")
    row:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8" })

    row.label = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    row.label:SetPoint("LEFT", 8, 0)
    row.label:SetJustifyH("LEFT")

    row.expLabels = {}
    for i = 1, EXPANSION_SLOTS do
        row.expLabels[i] = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        row.expLabels[i]:SetJustifyH("CENTER")
    end
    return row
end)

local charHeaderPool = HDG_UIHelpers.CreateRowPool(function(parent)
    local row = CreateFrame("Frame", nil, parent, "BackdropTemplate")
    row:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8" })

    row.label = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    row.label:SetPoint("LEFT", 8, 0)

    row.lumberText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    row.lumberText:SetPoint("RIGHT", row, "RIGHT", -24, 0)
    row.lumberText:SetJustifyH("RIGHT")

    row.lumberIcon = row:CreateTexture(nil, "OVERLAY")
    row.lumberIcon:SetSize(16, 16)
    row.lumberIcon:SetPoint("LEFT", row.lumberText, "RIGHT", 4, 0)
    return row
end)

local dividerPool = HDG_UIHelpers.CreateRowPool(function(parent)
    return parent:CreateTexture(nil, "ARTWORK")
end)

local fontStringPool = HDG_UIHelpers.CreateRowPool(function(parent)
    return parent:CreateFontString(nil, "OVERLAY", "GameFontNormal")
end)

local function BoostRGB(r, g, b)
    local k = 1.8
    return math.min(r * k, 1), math.min(g * k, 1), math.min(b * k, 1)
end

local legendPipPool = HDG_UIHelpers.CreateRowPool(function(parent)
    local tex = parent:CreateTexture(nil, "ARTWORK")
    tex:SetTexture("Interface\\COMMON\\Indicator-Green")
    tex:SetDesaturated(true)
    return tex
end)

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

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

-- Cache for recipe counts per profession/expansion.
-- recipeCountCache[profName][expDisplayName] = { total = n, recipes = {spellID,...} }
-- Derived from HDG_DecorDB, which is static after load — build once per session,
-- never invalidated. Per-character known counts live in a separate path
-- (GetKnownRecipeCountForChar) and are invalidated via TRADE_SKILL_LIST_UPDATE
-- / NEW_RECIPE_LEARNED elsewhere.
local recipeCountCache = nil

-- Build recipe count cache from HDG_DecorDB
local function BuildRecipeCountCache()
    if recipeCountCache then return recipeCountCache end

    recipeCountCache = {}

    if not HDG_DecorDB then
        return recipeCountCache
    end

    -- Count total recipes per profession/expansion
    -- Note: DecorDB is keyed by recipeID, spellID is inside the recipe object
    for recipeID, recipe in pairs(HDG_DecorDB) do
        local profName = recipe.profession
        local expansion = recipe.expansion
        local spellID = recipe.spellID  -- Use the actual spellID from the recipe
        if profName and expansion and spellID then
            -- Normalize expansion name (e.g., "Outland Alchemy" -> "The Burning Crusade")
            local expDisplay = HDG_ExpansionData.NormalizeToDisplayName(expansion)

            if not recipeCountCache[profName] then
                recipeCountCache[profName] = {}
            end
            if not recipeCountCache[profName][expDisplay] then
                recipeCountCache[profName][expDisplay] = { total = 0, recipes = {} }
            end

            recipeCountCache[profName][expDisplay].total = recipeCountCache[profName][expDisplay].total + 1
            table.insert(recipeCountCache[profName][expDisplay].recipes, spellID)
        end
    end

    return recipeCountCache
end

-- Get known recipe count for any character (uses stored knownRecipes data)
-- Returns: known (number or nil if no data), total
local function GetKnownRecipeCountForChar(charKey, profName, expDisplayName)
    local cache = BuildRecipeCountCache()
    if not cache[profName] or not cache[profName][expDisplayName] then
        return nil, 0
    end

    local data = cache[profName][expDisplayName]
    local total = data.total

    -- Get stored known recipes for this character
    local charData = HDG_Settings.characters and HDG_Settings.characters[charKey]
    if not charData or not charData.professions or not charData.professions[profName] then
        return nil, total  -- nil = no data for this char/prof
    end

    local profData = charData.professions[profName]
    if not profData.knownRecipes then
        return nil, total  -- nil = no recipe data stored
    end

    -- Count how many of the decor recipes this character knows
    local known = 0
    for _, spellID in ipairs(data.recipes) do
        if profData.knownRecipes[spellID] then
            known = known + 1
        end
    end

    return known, total
end

-- Convenience: Get known recipe count for current character
local function GetKnownRecipeCount(profName, expDisplayName)
    local currentChar = HDG_Data.GetCharacterKey()
    return GetKnownRecipeCountForChar(currentChar, profName, expDisplayName)
end

-- Get best known recipe count across all characters
-- Returns: bestKnown (number or nil), total, bestCharKey
local function GetBestKnownRecipeCount(profName, expDisplayName)
    local cache = BuildRecipeCountCache()
    if not cache[profName] or not cache[profName][expDisplayName] then
        return nil, 0, nil
    end

    local data = cache[profName][expDisplayName]
    local total = data.total
    local bestKnown = nil
    local bestChar = nil

    -- Check all characters
    if HDG_Settings.characters then
        for charKey, charData in pairs(HDG_Settings.characters) do
            if charData.professions and charData.professions[profName] then
                local profData = charData.professions[profName]
                if profData.knownRecipes then
                    local known = 0
                    for _, spellID in ipairs(data.recipes) do
                        if profData.knownRecipes[spellID] then
                            known = known + 1
                        end
                    end
                    if bestKnown == nil or known > bestKnown then
                        bestKnown = known
                        bestChar = charKey
                    end
                end
            end
        end
    end

    return bestKnown, total, bestChar
end

-- Get total recipe count (no knowledge check)
local function GetTotalRecipeCount(profName, expDisplayName)
    local cache = BuildRecipeCountCache()
    if not cache[profName] or not cache[profName][expDisplayName] then
        return 0
    end
    return cache[profName][expDisplayName].total
end

-- Helper: Look up skill data from SavedVariables by display name
-- Handles legacy data where API names (Outland, Northrend, etc.) were stored
local function GetSkillDataByDisplayName(skillLevels, displayName)
    if not skillLevels then return nil end

    -- Try display name first (new format)
    if skillLevels[displayName] then
        return skillLevels[displayName]
    end

    -- Fall back to checking all stored keys and normalizing them
    for storedName, data in pairs(skillLevels) do
        local normalizedStored = HDG_ExpansionData.NormalizeApiName(storedName)
        if normalizedStored == displayName then
            return data
        end
    end

    return nil
end

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

function HDG_AltsTab.Init(frame)
    mainFrame = frame
end

-- ============================================================================
-- CREATE PANEL
-- ============================================================================

function HDG_AltsTab.Create()
    if not mainFrame then return end

    -- Use factory to create panel with header and scroll
    local layout = HDG.UI:TabPanel(mainFrame, {
        title = L["ALTS_PANEL_TITLE"] or "Your Characters",
        hasHeaderBar = true,
        hasScroll = true,
    })

    local charactersFrame = layout.frame
    charactersFrame:Hide()
    mainFrame.charactersFrame = charactersFrame
    mainFrame.charactersContent = layout.scrollChild

    -- View mode toggle buttons (Skills | Recipes) - in header bar
    local btnWidth = 60
    local btnHeight = 18
    local scheme = GetScheme()

    local recipesBtn = CreateFrame("Button", nil, layout.headerBg, "BackdropTemplate")
    recipesBtn:SetPoint("RIGHT", -8, 0)
    recipesBtn:SetSize(btnWidth, btnHeight)
    recipesBtn:SetBackdrop({
        bgFile = CONFIG.TEXTURES.WHITE_BG,
        edgeFile = CONFIG.TEXTURES.WHITE_BG,
        edgeSize = 1,
    })
    recipesBtn.text = recipesBtn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    recipesBtn.text:SetPoint("CENTER", 0, 0)
    ApplyFont(recipesBtn.text, scheme, "small")
    recipesBtn.text:SetText(L["ALTS_VIEW_RECIPES"] or "Recipes")
    RegisterWidget(recipesBtn, "FilterButton")

    local skillsBtn = CreateFrame("Button", nil, layout.headerBg, "BackdropTemplate")
    skillsBtn:SetPoint("RIGHT", recipesBtn, "LEFT", -2, 0)
    skillsBtn:SetSize(btnWidth, btnHeight)
    skillsBtn:SetBackdrop({
        bgFile = CONFIG.TEXTURES.WHITE_BG,
        edgeFile = CONFIG.TEXTURES.WHITE_BG,
        edgeSize = 1,
    })
    skillsBtn.text = skillsBtn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    skillsBtn.text:SetPoint("CENTER", 0, 0)
    ApplyFont(skillsBtn.text, scheme, "small")
    skillsBtn.text:SetText(L["ALTS_VIEW_SKILLS"] or "Skills")
    RegisterWidget(skillsBtn, "FilterButton")

    local function UpdateToggleAppearance()
        local viewMode = HDG_Settings.altsViewMode or "skills"
        skillsBtn.isActive = (viewMode == "skills")
        recipesBtn.isActive = (viewMode == "recipes")
        local tScheme = GetScheme()
        if not tScheme then return end
        -- Active button
        local activeBtn = skillsBtn.isActive and skillsBtn or recipesBtn
        local inactiveBtn = skillsBtn.isActive and recipesBtn or skillsBtn
        activeBtn:SetBackdropColor(tScheme.accent.r, tScheme.accent.g, tScheme.accent.b, 1)
        activeBtn:SetBackdropBorderColor(tScheme.accent.r, tScheme.accent.g, tScheme.accent.b, 1)
        activeBtn.text:SetTextColor(tScheme.text.r, tScheme.text.g, tScheme.text.b)
        inactiveBtn:SetBackdropColor(tScheme.panel.r, tScheme.panel.g, tScheme.panel.b, 0.8)
        inactiveBtn:SetBackdropBorderColor(tScheme.border.r, tScheme.border.g, tScheme.border.b, 1)
        inactiveBtn.text:SetTextColor(tScheme.text_dim.r, tScheme.text_dim.g, tScheme.text_dim.b)
    end

    skillsBtn:SetScript("OnClick", function()
        HDG_Settings.altsViewMode = "skills"
        UpdateToggleAppearance()
        HDG_AltsTab.Refresh()
    end)

    recipesBtn:SetScript("OnClick", function()
        HDG_Settings.altsViewMode = "recipes"
        UpdateToggleAppearance()
        HDG_AltsTab.Refresh()
    end)

    UpdateToggleAppearance()
    mainFrame.altsSkillsBtn = skillsBtn
    mainFrame.altsRecipesBtn = recipesBtn

    -- "Hidden (N)" toggle button (positioned in Refresh next to Character Details header)
    local showHiddenBtn = CreateFrame("Button", nil, layout.scrollChild, "BackdropTemplate")
    showHiddenBtn:SetSize(btnWidth + 20, btnHeight)
    showHiddenBtn:SetBackdrop({
        bgFile = CONFIG.TEXTURES.WHITE_BG,
        edgeFile = CONFIG.TEXTURES.WHITE_BG,
        edgeSize = 1,
    })
    showHiddenBtn.text = showHiddenBtn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    showHiddenBtn.text:SetPoint("CENTER", 0, 0)
    ApplyFont(showHiddenBtn.text, scheme, "small")
    RegisterWidget(showHiddenBtn, "FilterButton")
    showHiddenBtn:Hide()

    local function UpdateHiddenBtnAppearance()
        local tScheme = GetScheme()
        if not tScheme then return end
        if showHiddenChars then
            showHiddenBtn:SetBackdropColor(tScheme.accent.r, tScheme.accent.g, tScheme.accent.b, 1)
            showHiddenBtn:SetBackdropBorderColor(tScheme.accent.r, tScheme.accent.g, tScheme.accent.b, 1)
            showHiddenBtn.text:SetTextColor(tScheme.text.r, tScheme.text.g, tScheme.text.b)
        else
            showHiddenBtn:SetBackdropColor(tScheme.panel.r, tScheme.panel.g, tScheme.panel.b, 0.8)
            showHiddenBtn:SetBackdropBorderColor(tScheme.border.r, tScheme.border.g, tScheme.border.b, 1)
            showHiddenBtn.text:SetTextColor(tScheme.text_dim.r, tScheme.text_dim.g, tScheme.text_dim.b)
        end
    end

    showHiddenBtn:SetScript("OnClick", function()
        showHiddenChars = not showHiddenChars
        UpdateHiddenBtnAppearance()
        HDG_AltsTab.Refresh()
    end)

    mainFrame.showHiddenBtn = showHiddenBtn
    mainFrame.UpdateHiddenBtnAppearance = UpdateHiddenBtnAppearance

    return charactersFrame
end

-- ============================================================================
-- REFRESH PANEL
-- ============================================================================

function HDG_AltsTab.Refresh()
    if not mainFrame or not mainFrame.charactersContent then return end

    local scheme = GetScheme()
    local content = mainFrame.charactersContent

    profRowPool:ReleaseAll(function(row)
        row.scanLabel:Hide()
        for i = 1, EXPANSION_SLOTS do
            row.skillBtns[i]:SetScript("OnEnter", nil)
            row.skillBtns[i]:SetScript("OnLeave", nil)
            row.recipePips[i]:Hide()
        end
    end)
    headerRowPool:ReleaseAll()
    charHeaderPool:ReleaseAll()
    dividerPool:ReleaseAll()
    fontStringPool:ReleaseAll()
    legendPipPool:ReleaseAll()

    -- Update "Hidden (N)" button visibility and label
    if mainFrame.showHiddenBtn then
        local hiddenCount = 0
        local _state = HDG.Store:GetState()
        local hiddenChars = _state and _state.ui and _state.ui.hiddenCharacters
        if hiddenChars then
            for _ in pairs(hiddenChars) do hiddenCount = hiddenCount + 1 end
        end
        if hiddenCount > 0 then
            mainFrame.showHiddenBtn.text:SetText(string.format(L["ALTS_HIDDEN_COUNT"], hiddenCount))
            mainFrame.showHiddenBtn:Show()
            if mainFrame.UpdateHiddenBtnAppearance then mainFrame.UpdateHiddenBtnAppearance() end
        else
            mainFrame.showHiddenBtn:Hide()
            showHiddenChars = false
        end
    end

    local yOffset = 0
    local EXPANSION_COL_WIDTH = 58
    local PROF_COL_WIDTH = 100

    -- Get expansion order and abbreviations from HDG_Data
    local expansionOrder = HDG_Data.EXPANSION_ORDER or {
        "Classic", "The Burning Crusade", "Wrath of the Lich King", "Cataclysm",
        "Mists of Pandaria", "Warlords of Draenor", "Legion", "Battle for Azeroth",
        "Shadowlands", "Dragonflight", "The War Within"
    }
    local expansionAbbr = HDG_Data.EXPANSION_ABBR or {}
    local numExpansions = #expansionOrder

    -- Crafting professions first, then gathering at the bottom
    local professionOrder = { "Alchemy", "Blacksmithing", "Cooking", "Enchanting", "Engineering",
        "Inscription", "Jewelcrafting", "Leatherworking", "Tailoring", "Herbalism", "Mining", "Skinning" }
    local gatheringProfessions = { ["Herbalism"] = true, ["Mining"] = true, ["Skinning"] = true }
    local firstGatheringProf = "Herbalism"

    -- Get all characters from saved data
    local characters = HDG_Settings.characters or {}
    local currentChar = HDG_Data.GetCharacterKey()
    local charList = {}

    -- Helper: check if character has any professions with scanned skill data
    local function hasProfessions(charKey)
        local charData = characters[charKey]
        if not charData or not charData.professions then return false end
        for _, profData in pairs(charData.professions) do
            if profData.skillLevels and next(profData.skillLevels) then
                return true
            end
        end
        return false
    end

    -- Sort characters: current first, then those with professions, then alphabetically
    for charKey, _ in pairs(characters) do
        table.insert(charList, charKey)
    end
    table.sort(charList, function(a, b)
        if a == currentChar then return true end
        if b == currentChar then return false end
        local aHasProf = hasProfessions(a)
        local bHasProf = hasProfessions(b)
        if aHasProf ~= bHasProf then
            return aHasProf
        end
        return a < b
    end)

    -- Filter out hidden characters (unless Show Hidden is active)
    if not showHiddenChars then
        local filtered = {}
        for _, charKey in ipairs(charList) do
            if not IsHidden(charKey) then
                table.insert(filtered, charKey)
            end
        end
        charList = filtered
    end

    -- If no characters yet, show message
    if #charList == 0 then
        local noDataText = fontStringPool:Acquire(content)
        noDataText:SetPoint("TOPLEFT", 8, yOffset)
        noDataText:SetText(HDG.Theme.C_Dim() .. (L["STATUS_NO_CHARACTER_DATA"] or "No character data yet. Open a profession window to scan skill levels.") .. "|r")
        ApplyFont(noDataText, scheme, "body")
        content:SetHeight(40)
        return
    end

    -- ========================================================================
    -- ACCOUNT SUMMARY GRID
    -- ========================================================================

    -- Build account-wide best skills data
    local accountBest = {}
    for _, profName in ipairs(professionOrder) do
        accountBest[profName] = {}
    end

    for charKey, charData in pairs(characters) do
        if (showHiddenChars or not IsHidden(charKey)) and charData.professions then
            for profName, profData in pairs(charData.professions) do
                if profData.skillLevels then
                    for expName, skillData in pairs(profData.skillLevels) do
                        local normalizedExpName = HDG_ExpansionData.NormalizeApiName(expName)
                        local current = skillData.current or 0
                        local max = skillData.max or 0
                        if not accountBest[profName] then
                            accountBest[profName] = {}
                        end
                        local existing = accountBest[profName][normalizedExpName]
                        if not existing then
                            accountBest[profName][normalizedExpName] = {
                                current = current,
                                max = max,
                                chars = { { name = charKey, current = current, max = max } }
                            }
                        else
                            table.insert(existing.chars, { name = charKey, current = current, max = max })
                            if current > existing.current then
                                existing.current = current
                                existing.max = max
                            end
                        end
                    end
                end
            end
        end
    end

    local viewMode = HDG_Settings.altsViewMode or "skills"

    -- Summary header
    local summaryHeader = fontStringPool:Acquire(content)
    summaryHeader:SetPoint("TOPLEFT", 8, yOffset)
    ApplyFont(summaryHeader, scheme, "body")
    if viewMode == "skills" then
        summaryHeader:SetText(HDG.Theme.C_Warning() .. (L["HEADER_ACCOUNT_SUMMARY"] or "Account Summary") .. "|r " .. HDG.Theme.C_Dim() .. "(best skill per profession)|r")
    else
        summaryHeader:SetText(HDG.Theme.C_Warning() .. (L["HEADER_ACCOUNT_SUMMARY"] or "Account Summary") .. "|r " .. HDG.Theme.C_Dim() .. "(known decor recipes)|r")
    end
    yOffset = yOffset - 20

    -- Expansion header row for summary
    local expHeader = headerRowPool:Acquire(content)
    expHeader:SetPoint("TOPLEFT", 0, yOffset)
    expHeader:SetSize(CONFIG.MAIN_FRAME_WIDTH - 60, 18)
    if scheme then
        expHeader:SetBackdropColor(scheme.panel.r, scheme.panel.g, scheme.panel.b, 0.9)
    else
        expHeader:SetBackdropColor(0.1, 0.15, 0.15, 0.9)
    end

    expHeader.label:SetWidth(PROF_COL_WIDTH)
    ApplyFont(expHeader.label, scheme, "small")
    expHeader.label:SetText(HDG.Theme.C_Text() .. "Profession|r")

    local xOffset = PROF_COL_WIDTH + 8
    for i, expName in ipairs(expansionOrder) do
        local expLabel = expHeader.expLabels[i]
        expLabel:ClearAllPoints()
        expLabel:SetPoint("LEFT", xOffset, 0)
        expLabel:SetWidth(EXPANSION_COL_WIDTH)
        expLabel:SetText(HDG.Theme.C_Text() .. (expansionAbbr[expName] or expName) .. "|r")
        ApplyFont(expLabel, scheme, "small")
        expLabel:Show()
        xOffset = xOffset + EXPANSION_COL_WIDTH
    end
    for i = numExpansions + 1, EXPANSION_SLOTS do
        expHeader.expLabels[i]:Hide()
    end
    yOffset = yOffset - 18

    -- Summary profession rows
    for _, profName in ipairs(professionOrder) do
        local localizedProfName = HDG_ExpansionData.GetLocalizedProfessionName(profName)

        -- Add small divider line before gathering professions
        if profName == firstGatheringProf then
            local gatheringDivider = dividerPool:Acquire(content)
            gatheringDivider:SetPoint("TOPLEFT", 8, yOffset - 2)
            gatheringDivider:SetSize(CONFIG.MAIN_FRAME_WIDTH - 76, 1)
            if scheme then
                gatheringDivider:SetColorTexture(scheme.border.r, scheme.border.g, scheme.border.b, 0.8)
            else
                gatheringDivider:SetColorTexture(0.25, 0.35, 0.35, 0.8)
            end
            yOffset = yOffset - 6
        end

        local profRow = profRowPool:Acquire(content)
        profRow:SetPoint("TOPLEFT", 0, yOffset)
        profRow:SetSize(CONFIG.MAIN_FRAME_WIDTH - 60, 18)
        if scheme then
            profRow:SetBackdropColor(scheme.bg.r, scheme.bg.g, scheme.bg.b, 0.4)
        else
            profRow:SetBackdropColor(0.05, 0.05, 0.05, 0.4)
        end

        profRow.profLabel:SetWidth(PROF_COL_WIDTH)
        ApplyFont(profRow.profLabel, scheme, "small")

        local bestRecipeCache = {}
        for _, expNameCheck in ipairs(expansionOrder) do
            local k, t, b = GetBestKnownRecipeCount(profName, expNameCheck)
            bestRecipeCache[expNameCheck] = { k, t, b }
        end

        local profColor
        if viewMode == "recipes" then
            local totalKnown = 0
            local totalRecipes = 0
            local hasAnyRecipeData = false
            for _, expNameCheck in ipairs(expansionOrder) do
                local cached = bestRecipeCache[expNameCheck]
                local known, total = cached[1], cached[2]
                totalRecipes = totalRecipes + total
                if known then
                    hasAnyRecipeData = true
                    totalKnown = totalKnown + known
                end
            end

            if totalRecipes == 0 then
                profColor = HDG.Theme.C_Text()
            elseif not hasAnyRecipeData then
                profColor = HDG.Theme.C_Text()
            elseif totalKnown == 0 then
                profColor = HDG.Theme.C_Dim()
            elseif totalKnown >= totalRecipes then
                profColor = HDG.Theme.C_Accent()
            else
                profColor = HDG.Theme.C_Warning()
            end
        else
            local anyBelowThreshold = false
            local hasAnyData = false
            for _, expNameCheck in ipairs(expansionOrder) do
                local bestData = accountBest[profName] and accountBest[profName][expNameCheck]
                if bestData then
                    hasAnyData = true
                    local current = bestData.current or 0
                    local decorThreshold = HDG_GetDecorThreshold and HDG_GetDecorThreshold(profName, expNameCheck) or 0
                    if decorThreshold > 0 and current < decorThreshold then
                        anyBelowThreshold = true
                        break
                    end
                end
            end

            if not hasAnyData then
                profColor = HDG.Theme.C_Text()
            elseif anyBelowThreshold then
                profColor = HDG.Theme.C_Warning()
            else
                profColor = HDG.Theme.C_Accent()
            end
        end
        profRow.profLabel:SetText(profColor .. localizedProfName .. "|r")

        xOffset = PROF_COL_WIDTH + 8
        for i, expName in ipairs(expansionOrder) do
            local bestData = accountBest[profName] and accountBest[profName][expName]
            local skillBtn = profRow.skillBtns[i]
            local skillLabel = profRow.skillLabels[i]

            skillBtn:ClearAllPoints()
            skillBtn:SetPoint("LEFT", xOffset, 0)
            skillBtn:SetSize(EXPANSION_COL_WIDTH, 18)
            skillBtn:Show()

            skillLabel:ClearAllPoints()
            skillLabel:SetPoint("CENTER", 0, 0)
            skillLabel:SetWidth(EXPANSION_COL_WIDTH)
            ApplyFont(skillLabel, scheme, "small")

            local pip = profRow.recipePips[i]

            if viewMode == "recipes" then
                pip:Hide()
                local cached = bestRecipeCache[expName]
                local known, total, bestChar = cached[1], cached[2], cached[3]

                local color
                if total == 0 then
                    color = HDG.Theme.C_Dim()
                    skillLabel:SetText(color .. "-|r")
                elseif known == nil then
                    skillLabel:SetText(HDG.Theme.C_Dim() .. string.format("?/%d|r", total))
                elseif known == 0 then
                    color = HDG.Theme.C_Dim()
                    skillLabel:SetText(string.format("%s0/%d|r", color, total))
                elseif known >= total then
                    color = HDG.Theme.C_Accent()
                    skillLabel:SetText(string.format("%s%d/%d|r", color, known, total))
                else
                    color = HDG.Theme.C_Warning()
                    skillLabel:SetText(string.format("%s%d/%d|r", color, known, total))
                end

                skillBtn:SetScript("OnEnter", function(self)
                    GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
                    GameTooltip:AddLine(localizedProfName .. " - " .. (expansionAbbr[expName] or expName), 1, 0.82, 0.3)
                    if total > 0 then
                        if known == nil then
                            GameTooltip:AddLine(HDG.Theme.C_Dim() .. L["ALTS_TIP_OPEN_PROF_SCAN"] .. "|r")
                        else
                            GameTooltip:AddLine(string.format(L["ALTS_TIP_BEST_RECIPES"], known, total))
                            if bestChar then
                                local charColor = (bestChar == currentChar) and HDG.Theme.C_Warning() or HDG.Theme.C_Text()
                                GameTooltip:AddLine(charColor .. bestChar .. "|r")
                            end
                            if known >= total then
                                GameTooltip:AddLine(HDG.Theme.C_Accent() .. L["ALTS_TIP_ALL_LEARNED"] .. "|r")
                            elseif known > 0 then
                                GameTooltip:AddLine(HDG.Theme.C_Warning() .. string.format(L["ALTS_TIP_RECIPES_REMAINING"], total - known) .. "|r")
                            end
                        end
                    else
                        GameTooltip:AddLine(HDG.Theme.C_Dim() .. L["ALTS_TIP_NO_RECIPES"] .. "|r")
                    end
                    GameTooltip:Show()
                end)
                skillBtn:SetScript("OnLeave", function() GameTooltip:Hide() end)
            elseif bestData then
                local current = bestData.current or 0
                local max = bestData.max or 0
                local decorThreshold = HDG_GetDecorThreshold and HDG_GetDecorThreshold(profName, expName) or nil
                decorThreshold = decorThreshold or 0

                local color
                if decorThreshold == 0 then
                    color = HDG.Theme.C_Dim()
                elseif current >= decorThreshold then
                    color = HDG.Theme.C_Accent()
                else
                    color = HDG.Theme.C_Warning()
                end
                skillLabel:SetText(string.format("%s%d|r", color, current))

                -- Recipe completion pip
                local rcached = bestRecipeCache[expName]
                local rKnown, rTotal = rcached[1], rcached[2]
                if rTotal > 0 and rKnown ~= nil then
                    pip:ClearAllPoints()
                    pip:SetPoint("LEFT", skillBtn, "CENTER", skillLabel:GetStringWidth() / 2 + 2, 0)
                    pip:Show()
                    pip:SetTexture("Interface\\COMMON\\Indicator-Green")
                    pip:SetDesaturated(true)
                    if rKnown >= rTotal then
                        pip:SetVertexColor(BoostRGB(HDG.Theme.RGB_Success()))
                    else
                        pip:SetVertexColor(BoostRGB(HDG.Theme.RGB_Error()))
                    end
                else
                    pip:Hide()
                end

                skillBtn:SetScript("OnEnter", function(self)
                    GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
                    GameTooltip:AddLine(localizedProfName .. " - " .. (expansionAbbr[expName] or expName), 1, 0.82, 0.3)
                    if decorThreshold and decorThreshold > 0 then
                        local thresholdMet = current >= decorThreshold
                        local statusColor = thresholdMet and HDG.Theme.C_Accent() or HDG.Theme.C_Warning()
                        local statusText = thresholdMet and "Can learn decor recipes" or "Below decor threshold"
                        GameTooltip:AddLine(statusColor .. statusText .. "|r")
                        GameTooltip:AddLine(HDG.Theme.C_Dim() .. string.format(L["ALTS_TIP_SKILL_REQUIRED"], decorThreshold) .. "|r")
                    else
                        GameTooltip:AddLine(HDG.Theme.C_Dim() .. L["ALTS_TIP_NO_RECIPES"] .. "|r")
                    end
                    GameTooltip:AddLine(" ")
                    table.sort(bestData.chars, function(a, b) return a.current > b.current end)
                    for _, charInfo in ipairs(bestData.chars) do
                        local charColor = (charInfo.name == currentChar) and HDG.Theme.C_Warning() or HDG.Theme.C_Text()
                        local skillColor = HDG.Theme.C_Dim()
                        GameTooltip:AddDoubleLine(
                            charColor .. charInfo.name .. "|r",
                            skillColor .. charInfo.current .. "/" .. charInfo.max .. "|r",
                            1, 1, 1, 1, 1, 1
                        )
                    end
                    -- Recipe status in tooltip
                    if rTotal > 0 then
                        GameTooltip:AddLine(" ")
                        if rKnown == nil then
                            GameTooltip:AddLine(HDG.Theme.C_Dim() .. L["ALTS_TIP_SCAN_RECIPES"] .. "|r")
                        elseif rKnown >= rTotal then
                            GameTooltip:AddLine(HDG.Theme.C_Accent() .. string.format(L["ALTS_TIP_RECIPES_COMPLETE"], rKnown, rTotal) .. "|r")
                        elseif rKnown > 0 then
                            GameTooltip:AddLine(HDG.Theme.C_Warning() .. string.format(L["ALTS_TIP_RECIPES_REMAINING_DETAIL"], rKnown, rTotal, rTotal - rKnown) .. "|r")
                        else
                            GameTooltip:AddLine(HDG.Theme.C_Dim() .. string.format(L["ALTS_TIP_RECIPES_ZERO"], rTotal) .. "|r")
                        end
                    end
                    GameTooltip:Show()
                end)
                skillBtn:SetScript("OnLeave", function() GameTooltip:Hide() end)
            else
                pip:Hide()
                skillLabel:SetText(HDG.Theme.C_Dim() .. "-|r")
            end

            xOffset = xOffset + EXPANSION_COL_WIDTH
        end
        -- Hide unused expansion slots
        for i = numExpansions + 1, EXPANSION_SLOTS do
            profRow.skillBtns[i]:Hide()
        end

        yOffset = yOffset - 18
    end

    -- Color legend
    yOffset = yOffset - 12
    local legendText = fontStringPool:Acquire(content)
    legendText:SetPoint("TOPLEFT", 8, yOffset)
    ApplyFont(legendText, scheme, "small")
    if viewMode == "recipes" then
        legendText:SetText(HDG.Theme.C_Accent() .. "All learned|r   " .. HDG.Theme.C_Warning() .. "Partial|r   " .. HDG.Theme.C_Dim() .. "None / No recipes|r")
    else
        legendText:SetText("Decor Recipes:  " .. HDG.Theme.C_Accent() .. "Skill sufficient|r / " .. HDG.Theme.C_Warning() .. "Insufficient skill|r / " .. HDG.Theme.C_Dim() .. "No recipes available|r    |    ")

        local sPip = legendPipPool:Acquire(content)
        sPip:SetSize(24, 24)
        sPip:SetVertexColor(BoostRGB(HDG.Theme.RGB_Success()))
        sPip:SetPoint("LEFT", legendText, "LEFT", legendText:GetStringWidth() + 2, 0)

        local sLabel = fontStringPool:Acquire(content)
        ApplyFont(sLabel, scheme, "small")
        sLabel:SetPoint("LEFT", sPip, "RIGHT", 4, 0)
        sLabel:SetText(HDG.Theme.C_Success() .. "All decor recipes known|r / ")

        local ePip = legendPipPool:Acquire(content)
        ePip:SetSize(24, 24)
        ePip:SetVertexColor(BoostRGB(HDG.Theme.RGB_Error()))
        ePip:SetPoint("LEFT", sLabel, "LEFT", sLabel:GetStringWidth() + 2, 0)

        local eLabel = fontStringPool:Acquire(content)
        ApplyFont(eLabel, scheme, "small")
        eLabel:SetPoint("LEFT", ePip, "RIGHT", 4, 0)
        eLabel:SetText(HDG.Theme.C_Warning() .. "Missing decor recipes|r")
    end
    yOffset = yOffset - 16

    -- Divider
    yOffset = yOffset - 8
    local divider = dividerPool:Acquire(content)
    divider:SetPoint("TOPLEFT", 8, yOffset)
    divider:SetSize(CONFIG.MAIN_FRAME_WIDTH - 76, 1)
    if scheme then
        divider:SetColorTexture(scheme.border.r, scheme.border.g, scheme.border.b, 0.5)
    else
        divider:SetColorTexture(0.3, 0.3, 0.3, 0.5)
    end
    yOffset = yOffset - 10

    -- ========================================================================
    -- CHARACTER DETAILS (per-character breakdown)
    -- ========================================================================

    local detailsHeader = fontStringPool:Acquire(content)
    detailsHeader:SetPoint("TOPLEFT", 8, yOffset)
    ApplyFont(detailsHeader, scheme, "body")
    detailsHeader:SetText(HDG.Theme.C_Warning() .. (L["ALTS_HEADER_CHAR_DETAILS"] or "Character Details") .. "|r")

    -- Position "Hidden (N)" button next to header
    if mainFrame.showHiddenBtn then
        mainFrame.showHiddenBtn:ClearAllPoints()
        mainFrame.showHiddenBtn:SetPoint("LEFT", detailsHeader, "RIGHT", 8, 0)
    end

    yOffset = yOffset - 24

    -- Split characters into those with and without professions
    local charsWithProfs = {}
    local charsWithoutProfs = {}
    for _, charKey in ipairs(charList) do
        if hasProfessions(charKey) then
            table.insert(charsWithProfs, charKey)
        else
            table.insert(charsWithoutProfs, charKey)
        end
    end

    -- Process characters with professions
    for _, charKey in ipairs(charsWithProfs) do
        local charData = characters[charKey]
        local isCurrentChar = (charKey == currentChar)

        -- Character header with name
        local charHeader = charHeaderPool:Acquire(content)
        charHeader:SetPoint("TOPLEFT", 0, yOffset)
        charHeader:SetSize(CONFIG.MAIN_FRAME_WIDTH - 60, 22)
        if scheme then
            charHeader:SetBackdropColor(scheme.panel.r, scheme.panel.g, scheme.panel.b, 0.9)
        else
            charHeader:SetBackdropColor(0.1, 0.15, 0.15, 0.9)
        end

        -- Pool may have been used as the "No Professions" section header
        -- previously, which lazy-attaches a collapseArrow and reanchors the
        -- label to LEFT 22. Reset both so per-char rows render cleanly.
        if charHeader.collapseArrow then charHeader.collapseArrow:Hide() end
        charHeader:EnableMouse(false)
        charHeader:SetScript("OnMouseUp", nil)
        charHeader.label:ClearAllPoints()
        charHeader.label:SetPoint("LEFT", 8, 0)

        ApplyFont(charHeader.label, scheme, "body")
        local isCharHidden = IsHidden(charKey)
        local charColor
        if isCharHidden then
            charColor = HDG.Theme.C_Dim()
        elseif isCurrentChar then
            charColor = HDG.Theme.C_Warning()
        else
            charColor = HDG.Theme.C_Text()
        end
        local currentIndicator = isCurrentChar and (L["ALTS_CURRENT"] or " (current)") or ""
        charHeader.label:SetText(charColor .. charKey .. currentIndicator .. "|r")

        -- Hide/show button (lazy-created on pooled row)
        if not charHeader.hideBtn then
            local btn = CreateFrame("Button", nil, charHeader)
            btn:SetSize(16, 16)
            btn:SetPoint("LEFT", charHeader.label, "RIGHT", 4, 0)
            btn.icon = btn:CreateTexture(nil, "OVERLAY")
            btn.icon:SetAllPoints()
            btn.icon:SetAtlas("transmog-icon-hidden")
            btn:SetScript("OnEnter", function(self)
                GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
                GameTooltip:AddLine(L["ALTS_TIP_HIDE_SHOW"], 1, 0.82, 0.3)
                GameTooltip:AddLine(L["ALTS_TIP_DATA_CONTINUES"], 0.6, 0.6, 0.6)
                GameTooltip:Show()
            end)
            btn:SetScript("OnLeave", function() GameTooltip:Hide() end)
            charHeader.hideBtn = btn
        end
        charHeader.hideBtn:Show()
        if isCharHidden then
            charHeader.hideBtn.icon:SetAtlas("transmog-icon-hidden")
            charHeader.hideBtn.icon:SetDesaturated(true)
            charHeader.hideBtn.icon:SetAlpha(0.5)
        else
            charHeader.hideBtn.icon:SetAtlas("transmog-icon-hidden")
            charHeader.hideBtn.icon:SetDesaturated(false)
            charHeader.hideBtn.icon:SetAlpha(1.0)
        end
        local capturedCharKey = charKey
        charHeader.hideBtn:SetScript("OnClick", function()
            SetHidden(capturedCharKey, not IsHidden(capturedCharKey))
        end)

        -- Delete character button (trashcan, right of hide button)
        if not charHeader.deleteBtn then
            local dbtn = CreateFrame("Button", nil, charHeader)
            dbtn:SetSize(14, 14)
            dbtn:SetPoint("LEFT", charHeader.hideBtn, "RIGHT", 4, 0)
            dbtn.icon = dbtn:CreateTexture(nil, "OVERLAY")
            dbtn.icon:SetAllPoints()
            dbtn.icon:SetAtlas("common-icon-delete")
            dbtn.icon:SetVertexColor(0.6, 0.3, 0.3, 0.7)
            dbtn:SetScript("OnEnter", function(self)
                self.icon:SetVertexColor(1, 0.3, 0.3, 1)
                GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
                GameTooltip:AddLine("Delete Character Data", 1, 0.3, 0.3)
                GameTooltip:AddLine("Removes all saved professions and lumber data for this character. Useful if you renamed or realm transferred a character.", 0.7, 0.7, 0.7, true)
                GameTooltip:Show()
            end)
            dbtn:SetScript("OnLeave", function(self)
                self.icon:SetVertexColor(0.6, 0.3, 0.3, 0.7)
                GameTooltip:Hide()
            end)
            charHeader.deleteBtn = dbtn
        end
        charHeader.deleteBtn:Show()
        local capturedDeleteKey = charKey
        charHeader.deleteBtn:SetScript("OnClick", function()
            StaticPopup_Show("HDG_CONFIRM_DELETE_CHAR", capturedDeleteKey, nil, capturedDeleteKey)
        end)

        -- Find Lumber indicator (use spell icon for Find Lumber)
        local lumberTex = (C_Spell and C_Spell.GetSpellTexture and C_Spell.GetSpellTexture(1256697)) or "Interface\\Icons\\INV_Tradeskillitem_03"
        charHeader.lumberIcon:SetTexture(lumberTex)
        ApplyFont(charHeader.lumberText, scheme, "small")
        if charData.knowsFindLumber == true then
            charHeader.lumberIcon:SetDesaturated(false)
            charHeader.lumberIcon:SetAlpha(1.0)
            charHeader.lumberText:SetText(HDG.Theme.C_Success() .. (L["ALTS_LUMBER_KNOWN"] or "Find Lumber known") .. "|r")
        elseif charData.knowsFindLumber == false then
            charHeader.lumberIcon:SetDesaturated(true)
            charHeader.lumberIcon:SetAlpha(0.4)
            charHeader.lumberText:SetText(HDG.Theme.C_Error() .. (L["ALTS_LUMBER_NOT_LEARNED"] or "Find Lumber unknown") .. "|r")
        else
            charHeader.lumberIcon:SetDesaturated(true)
            charHeader.lumberIcon:SetAlpha(0.3)
            charHeader.lumberText:SetText(HDG.Theme.C_Dim() .. (L["ALTS_LOG_IN"] or "Log in to check") .. "|r")
        end

        yOffset = yOffset - 24

        -- Expansion header row (abbreviations)
        local charExpHeader = headerRowPool:Acquire(content)
        charExpHeader:SetPoint("TOPLEFT", 0, yOffset)
        charExpHeader:SetSize(CONFIG.MAIN_FRAME_WIDTH - 60, 18)
        if scheme then
            charExpHeader:SetBackdropColor(scheme.panel.r, scheme.panel.g, scheme.panel.b, 0.9)
        else
            charExpHeader:SetBackdropColor(0.1, 0.15, 0.15, 0.9)
        end

        charExpHeader.label:SetWidth(PROF_COL_WIDTH)
        ApplyFont(charExpHeader.label, scheme, "small")
        charExpHeader.label:SetText(HDG.Theme.C_Text() .. "Profession|r")

        xOffset = PROF_COL_WIDTH + 8
        for i, expName in ipairs(expansionOrder) do
            local expLabel = charExpHeader.expLabels[i]
            expLabel:ClearAllPoints()
            expLabel:SetPoint("LEFT", xOffset, 0)
            expLabel:SetWidth(EXPANSION_COL_WIDTH)
            expLabel:SetText(HDG.Theme.C_Text() .. (expansionAbbr[expName] or expName) .. "|r")
            ApplyFont(expLabel, scheme, "small")
            expLabel:Show()
            xOffset = xOffset + EXPANSION_COL_WIDTH
        end
        for i = numExpansions + 1, EXPANSION_SLOTS do
            charExpHeader.expLabels[i]:Hide()
        end

        yOffset = yOffset - 18

        -- Profession skill level rows for this character
        local charHasProfessions = false
        local gatheringDividerShown = false
        for _, profName in ipairs(professionOrder) do
            local localizedProfNameChar = HDG_ExpansionData.GetLocalizedProfessionName(profName)
            local profData = charData.professions and charData.professions[profName]
            if profData then
                -- Add small divider line before gathering professions (only once per character)
                if not gatheringDividerShown and gatheringProfessions[profName] then
                    local gatheringDivider = dividerPool:Acquire(content)
                    gatheringDivider:SetPoint("TOPLEFT", 8, yOffset - 2)
                    gatheringDivider:SetSize(CONFIG.MAIN_FRAME_WIDTH - 76, 1)
                    if scheme then
                        gatheringDivider:SetColorTexture(scheme.border.r, scheme.border.g, scheme.border.b, 0.8)
                    else
                        gatheringDivider:SetColorTexture(0.25, 0.35, 0.35, 0.8)
                    end
                    yOffset = yOffset - 6
                    gatheringDividerShown = true
                end
                charHasProfessions = true
                local hasSkillData = profData.skillLevels and next(profData.skillLevels)

                -- Check if any expansion is below decor threshold for this character's profession
                local anyBelowThreshold = false
                if hasSkillData then
                    for _, expName in ipairs(expansionOrder) do
                        local skillData = GetSkillDataByDisplayName(profData.skillLevels, expName)
                        if skillData then
                            local current = skillData.current or 0
                            local decorThreshold = HDG_GetDecorThreshold and HDG_GetDecorThreshold(profName, expName) or 0
                            if decorThreshold > 0 and current < decorThreshold then
                                anyBelowThreshold = true
                                break
                            end
                        end
                    end
                end

                -- Profession row (pooled)
                local profRow = profRowPool:Acquire(content)
                profRow:SetPoint("TOPLEFT", 0, yOffset)
                profRow:SetSize(CONFIG.MAIN_FRAME_WIDTH - 60, 18)
                if scheme then
                    profRow:SetBackdropColor(scheme.bg.r, scheme.bg.g, scheme.bg.b, 0.4)
                else
                    profRow:SetBackdropColor(0.05, 0.05, 0.05, 0.4)
                end

                profRow.profLabel:SetWidth(PROF_COL_WIDTH)
                ApplyFont(profRow.profLabel, scheme, "small")
                local profColor
                if not hasSkillData then
                    profColor = HDG.Theme.C_Text()
                elseif anyBelowThreshold then
                    profColor = HDG.Theme.C_Warning()
                else
                    profColor = HDG.Theme.C_Accent()
                end
                profRow.profLabel:SetText(profColor .. localizedProfNameChar .. "|r")

                -- Expansion skill columns
                xOffset = PROF_COL_WIDTH + 8

                if viewMode == "recipes" then
                    for i, expName in ipairs(expansionOrder) do
                        local skillLabel = profRow.skillLabels[i]
                        local skillBtn = profRow.skillBtns[i]

                        skillBtn:ClearAllPoints()
                        skillBtn:SetPoint("LEFT", xOffset, 0)
                        skillBtn:SetSize(EXPANSION_COL_WIDTH, 18)
                        skillBtn:Show()
                        profRow.recipePips[i]:Hide()

                        skillLabel:ClearAllPoints()
                        skillLabel:SetPoint("CENTER", 0, 0)
                        ApplyFont(skillLabel, scheme, "small")
                        skillLabel:SetWidth(EXPANSION_COL_WIDTH)

                        local known, total = GetKnownRecipeCountForChar(charKey, profName, expName)
                        if total == 0 then
                            skillLabel:SetText(HDG.Theme.C_Dim() .. "-|r")
                        elseif known == nil then
                            skillLabel:SetText(HDG.Theme.C_Dim() .. string.format("?/%d|r", total))
                        else
                            local color
                            if known == 0 then
                                color = HDG.Theme.C_Dim()
                            elseif known >= total then
                                color = HDG.Theme.C_Accent()
                            else
                                color = HDG.Theme.C_Warning()
                            end
                            skillLabel:SetText(string.format("%s%d/%d|r", color, known, total))
                        end

                        xOffset = xOffset + EXPANSION_COL_WIDTH
                    end
                elseif hasSkillData then
                    for i, expName in ipairs(expansionOrder) do
                        local skillData = GetSkillDataByDisplayName(profData.skillLevels, expName)
                        local skillLabel = profRow.skillLabels[i]
                        local skillBtn = profRow.skillBtns[i]

                        skillBtn:ClearAllPoints()
                        skillBtn:SetPoint("LEFT", xOffset, 0)
                        skillBtn:SetSize(EXPANSION_COL_WIDTH, 18)
                        skillBtn:Show()

                        skillLabel:ClearAllPoints()
                        skillLabel:SetPoint("CENTER", 0, 0)
                        skillLabel:SetWidth(EXPANSION_COL_WIDTH)
                        ApplyFont(skillLabel, scheme, "small")

                        local pcPip = profRow.recipePips[i]
                        if skillData then
                            local current = skillData.current or 0
                            local max = skillData.max or 0
                            local decorThreshold = HDG_GetDecorThreshold and HDG_GetDecorThreshold(profName, expName) or 0

                            local color
                            if decorThreshold == 0 then
                                color = HDG.Theme.C_Dim()
                            elseif current >= decorThreshold then
                                color = HDG.Theme.C_Accent()
                            else
                                color = HDG.Theme.C_Warning()
                            end
                            skillLabel:SetText(string.format("%s%d/%d|r", color, current, max))

                            -- Recipe completion pip
                            local rKnown, rTotal = GetKnownRecipeCountForChar(charKey, profName, expName)
                            if rTotal > 0 and rKnown ~= nil then
                                pcPip:ClearAllPoints()
                                pcPip:SetPoint("LEFT", skillBtn, "CENTER", skillLabel:GetStringWidth() / 2 + 2, 0)
                                pcPip:Show()
                                pcPip:SetTexture("Interface\\COMMON\\Indicator-Green")
                                pcPip:SetDesaturated(true)
                                if rKnown >= rTotal then
                                    pcPip:SetVertexColor(BoostRGB(HDG.Theme.RGB_Success()))
                                else
                                    pcPip:SetVertexColor(BoostRGB(HDG.Theme.RGB_Error()))
                                end
                            else
                                pcPip:Hide()
                            end
                        else
                            pcPip:Hide()
                            skillLabel:SetText(HDG.Theme.C_Dim() .. "-|r")
                        end

                        xOffset = xOffset + EXPANSION_COL_WIDTH
                    end
                else
                    -- No skill data yet - hide all expansion buttons, show scan message
                    for i = 1, numExpansions do
                        profRow.skillBtns[i]:Hide()
                    end
                    profRow.scanLabel:ClearAllPoints()
                    profRow.scanLabel:SetPoint("LEFT", xOffset, 0)
                    profRow.scanLabel:SetText(HDG.Theme.C_Dim() .. "(" .. (L["ALTS_SCAN_INSTRUCTIONS"] or "Open a profession window on each character to scan their skill levels.") .. ")|r")
                    ApplyFont(profRow.scanLabel, scheme, "small")
                    profRow.scanLabel:Show()
                end

                -- Hide unused expansion slots
                for i = numExpansions + 1, EXPANSION_SLOTS do
                    profRow.skillBtns[i]:Hide()
                end

                yOffset = yOffset - 18
            end
        end

        yOffset = yOffset - 16  -- Gap between characters
    end

    -- Collapsible "No Professions" section for characters without any professions
    if #charsWithoutProfs > 0 then
        local sectionHeader = charHeaderPool:Acquire(content)
        sectionHeader:SetPoint("TOPLEFT", 0, yOffset)
        sectionHeader:SetSize(CONFIG.MAIN_FRAME_WIDTH - 60, 22)
        if scheme then
            sectionHeader:SetBackdropColor(scheme.panel.r, scheme.panel.g, scheme.panel.b, 0.6)
        else
            sectionHeader:SetBackdropColor(0.1, 0.15, 0.15, 0.6)
        end

        -- Collapse arrow (created lazily on pooled header)
        if not sectionHeader.collapseArrow then
            sectionHeader.collapseArrow = CreateFrame("Button", nil, sectionHeader)
            sectionHeader.collapseArrow:SetSize(16, 16)
            sectionHeader.collapseArrow:SetPoint("LEFT", 4, 0)
            sectionHeader.collapseArrow:SetHighlightAtlas("common-button-collapseExpand-hover")
        end
        if noProfCollapsed then
            sectionHeader.collapseArrow:SetNormalAtlas("common-button-collapseExpand-up")
            sectionHeader.collapseArrow:SetPushedAtlas("common-button-collapseExpand-up-pressed")
        else
            sectionHeader.collapseArrow:SetNormalAtlas("common-button-collapseExpand-down")
            sectionHeader.collapseArrow:SetPushedAtlas("common-button-collapseExpand-down-pressed")
        end
        sectionHeader.collapseArrow:Show()

        ApplyFont(sectionHeader.label, scheme, "body")
        sectionHeader.label:SetPoint("LEFT", 22, 0)
        sectionHeader.label:SetText(HDG.Theme.C_Dim() .. (L["ALTS_NO_PROFESSIONS"] or "No Professions / Not Scanned") .. " (" .. #charsWithoutProfs .. ")|r")
        -- Hide per-char widgets and lumber elements on this header
        if sectionHeader.hideBtn then sectionHeader.hideBtn:Hide() end
        if sectionHeader.deleteBtn then sectionHeader.deleteBtn:Hide() end
        sectionHeader.lumberIcon:SetAlpha(0)
        sectionHeader.lumberText:SetText("")

        local function toggleNoProf()
            noProfCollapsed = not noProfCollapsed
            HDG_AltsTab.Refresh()
        end
        sectionHeader:EnableMouse(true)
        sectionHeader:SetScript("OnMouseUp", toggleNoProf)
        sectionHeader.collapseArrow:SetScript("OnClick", toggleNoProf)

        yOffset = yOffset - 24

        if not noProfCollapsed then
            for _, charKey in ipairs(charsWithoutProfs) do
                local isCurrentChar = (charKey == currentChar)
                local nameText = fontStringPool:Acquire(content)
                nameText:SetPoint("TOPLEFT", 16, yOffset)
                ApplyFont(nameText, scheme, "small")
                local nameColor = isCurrentChar and HDG.Theme.C_Warning() or HDG.Theme.C_Dim()
                local indicator2 = isCurrentChar and (L["ALTS_CURRENT"] or " (current)") or ""
                nameText:SetText(nameColor .. charKey .. indicator2 .. "|r")
                yOffset = yOffset - 18
            end
        end

        yOffset = yOffset - 8
    end

    -- Info text at bottom
    yOffset = yOffset - 10
    local infoText = fontStringPool:Acquire(content)
    infoText:SetPoint("TOPLEFT", 8, yOffset)
    ApplyFont(infoText, scheme, "small")
    infoText:SetText(HDG.Theme.C_Dim() .. (L["ALTS_SCAN_INSTRUCTIONS"] or "Open a profession window on each character to scan their skill levels.") .. "|r")

    -- Update scroll content height
    content:SetHeight(math.abs(yOffset) + 20)
end

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

function HDG_AltsTab.Show()
    if mainFrame and mainFrame.charactersFrame then
        mainFrame.charactersFrame:Show()
    end
    altsDirty = false
    HDG_AltsTab.Refresh()
end

function HDG_AltsTab.Hide()
    if mainFrame and mainFrame.charactersFrame then
        mainFrame.charactersFrame:Hide()
    end
end

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

local function RegisterEventBusHandlers()
    local HDG = _G.HousingDecorGuide
    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

        -- Character data changes — defer if tab not visible
        if action == "UPDATE_CHARACTER" then
            if mainFrame and mainFrame.charactersFrame and mainFrame.charactersFrame:IsShown() then
                HDG_AltsTab.Refresh()
            else
                altsDirty = true
            end
        end
    end)

    HDG.EventBus:Register("HDG_PROFESSION_DATA_UPDATED", function()
        if mainFrame and mainFrame.charactersFrame and mainFrame.charactersFrame:IsShown() then
            HDG_AltsTab.Refresh()
        else
            altsDirty = true
        end
    end)
end

-- Register handlers after a short delay to ensure EventBus is ready
RegisterEventBusHandlers()

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

_G.HDG_AltsTab = HDG_AltsTab

return HDG_AltsTab
