-- Housing Decor Guide - Acquisition Tab
-- Panel layout, lifecycle, detail panels, refresh orchestration.
-- Data/filtering in HDG_AcqData.lua, row factories in HDG_AcqRows.lua.

local addonName, addon = ...

-- Module namespace
HDG_AcquisitionTab = HDG_AcquisitionTab or {}

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

local HDG = _G.HousingDecorGuide
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

local function C_Text() return HDG.Theme.C_Text() end
local function C_Dim() return HDG.Theme.C_Dim() end
local function C_Accent() return HDG.Theme.C_Accent() end
local function C_Success() return HDG.Theme.C_Success() end
local function C_Warning() return HDG.Theme.C_Warning() end

-- Local references (set during init)
local CONFIG
local mainFrame

-- Module frame references
local acquisitionPanel = nil
local selectedItemID = nil
local selectedVendor = nil
local galleryVendorNPC = nil -- npcID currently shown in "Also sells" gallery

-- Forward declarations
local RefreshGuideText
local ShowNotesExportWindow

-- Zone/expansion data from HDG_ExpansionData (SSoT)
local ZONE_MAP_IDS = HDG_ExpansionData.ZONE_MAP_IDS
local GetExpansionForMapID = HDG_ExpansionData.GetExpansionForMapID

-- View mode state
local viewMode = "vendor"
local expandedVendors = {}
local reusableItemList = {} -- reused per vendor expand to avoid table alloc

-- Hoisted sort comparator for vendor sub-item lists
local function sortByItemName(a, b)
    return (HDG_Data.GetCachedItemName(a) or "") < (HDG_Data.GetCachedItemName(b) or "")
end

-- Search debounce timer
local searchDebounceTimer = nil

-- Virtual scroll state (By Item view with large result sets)
local virtualScrollActive = false
local virtualScrollFirstIdx = 0
local virtualScrollDebounce = nil

-- Delegate to shared implementation in VendorResolver
local function ZonePctToMap(mapID, pctX, pctY)
    return HDG_VendorResolver.ZonePctToMap(mapID, pctX, pctY)
end

-- Check if coords are valid zone-percentage (0-100 range, not zero)
local function HasValidCoords(vendor)
    if not vendor or not vendor.mapID or vendor.mapID == 0 then return false end
    if not vendor.x or not vendor.y or (vendor.x == 0 and vendor.y == 0) then return false end
    if vendor.x < -5 or vendor.x > 105 or vendor.y < -5 or vendor.y > 105 then return false end
    return true
end

-- ============================================================================
-- PANEL CREATION
-- ============================================================================

function HDG_AcquisitionTab:CreatePanel()
    if acquisitionPanel then return acquisitionPanel end

    local ACQ = CONFIG.ACQUISITION_TAB
    local scheme = GetScheme()

    -- Use factory for main panel (no header/scroll - custom layout)
    local layout = HDG.UI:TabPanel(mainFrame, {
        hasHeaderBar = false,
        hasScroll = false,
    })

    acquisitionPanel = layout.frame
    acquisitionPanel:Hide()

    -- ========================================================================
    -- HEADER BAR
    -- ========================================================================

    local headerBg = CreateFrame("Frame", nil, acquisitionPanel, "BackdropTemplate")
    headerBg:SetPoint("TOPLEFT", 1, -1)
    headerBg:SetPoint("TOPRIGHT", -1, -1)
    headerBg:SetHeight(22)
    headerBg:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", tile = false })
    RegisterWidget(headerBg, "HeaderBar")

    local title = headerBg:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    title:SetPoint("CENTER", 0, 0)
    if scheme then
        ApplyFont(title, scheme, "header")
        title:SetTextColor(scheme.text_header.r, scheme.text_header.g, scheme.text_header.b, scheme.text_header.a)
    end
    title:SetText("Acquisition Guide")
    title.isHeader = true
    HDG.Theme.StyleHeaderTitle(title, headerBg)
    acquisitionPanel.headerTitle = title

    -- Help button — toggles guide panel
    local helpBtn = HDG.UI:Button(headerBg, CreateAtlasMarkup("QuestTurnin", 14, 14) .. " How to Use", 100, 18, function()
        if acquisitionPanel.placeholder:IsShown() then
            acquisitionPanel.placeholder:Hide()
            if selectedItemID then
                acquisitionPanel.headerPanel:Show()
                acquisitionPanel.detailPanel:Show()
            end
        else
            selectedItemID = nil
            selectedVendor = nil
            galleryVendorNPC = nil
            acquisitionPanel.headerPanel:Hide()
            acquisitionPanel.detailPanel:Hide()
            acquisitionPanel.galleryPanel:Hide()
            acquisitionPanel.infoPanel:Hide()
            acquisitionPanel.waypointBtn:Hide()
            acquisitionPanel.mapBtn:Hide()
            acquisitionPanel.placeholder:Show()
            RefreshGuideText()
            HDG_AcqRows.UpdateRowHighlights(nil)
        end
    end)
    helpBtn:SetPoint("RIGHT", headerBg, "RIGHT", -4, 1)
    acquisitionPanel.helpBtn = helpBtn

    -- Shopping List toggle in title bar (left side)
    local shopListBtn = HDG.UI:Button(headerBg, "Shopping List", 120, 18, function()
        if HDG_VendorShoppingList then HDG_VendorShoppingList.Toggle("list") end
    end)
    shopListBtn:SetPoint("LEFT", headerBg, "LEFT", 14, 1)
    acquisitionPanel.shopListBtn = shopListBtn
    HDG_AcqRows.shopListBtn = shopListBtn

    -- Hide Unreleased checkbox in title bar (default on — hides items not in AllDecorDB)
    local releasedCheck = HDG.UI:Checkbox(headerBg, 14, "Hide Unreleased")
    releasedCheck:SetPoint("LEFT", shopListBtn, "RIGHT", 8, 0)
    releasedCheck:SetChecked(true)
    releasedCheck.onChanged = function(checked)
        HDG_AcqData.filters.releasedOnly = checked
        HDG_AcquisitionTab:RefreshList()
    end
    acquisitionPanel.releasedCheck = releasedCheck

    -- Export notes button in title bar
    local exportNotesBtn = HDG.UI:Button(headerBg, "Export Notes", 85, 18, function()
        ShowNotesExportWindow()
    end)
    exportNotesBtn:SetPoint("RIGHT", helpBtn, "LEFT", -4, 0)
    HDG_UIHelpers.SetupTooltip(exportNotesBtn, "Export Notes", "Export all user notes to a text window for copy/paste.", "ANCHOR_BOTTOM")
    acquisitionPanel.exportNotesBtn = exportNotesBtn

    -- ========================================================================
    -- LEFT PANEL (Item List) - 40% width
    -- ========================================================================

    local panelWidth = acquisitionPanel:GetWidth()
    if panelWidth == 0 then panelWidth = CONFIG.MAIN_FRAME_WIDTH - 8 end
    local leftWidth = math.floor(panelWidth * ACQ.LEFT_PANEL_PCT)
    local rightWidth = panelWidth - leftWidth - 4

    local leftPanel = HDG.UI:Panel(acquisitionPanel, { width = leftWidth })
    leftPanel:SetPoint("TOPLEFT", 1, -24)
    leftPanel:SetPoint("BOTTOMLEFT", 1, 1)
    leftPanel:SetWidth(leftWidth)

    -- ========================================================================
    -- ROW 1: VIEW TOGGLE + MAP ALL (right-aligned)
    -- ========================================================================

    local viewRowY = -4

    local byVendorBtn = HDG.UI:TabButton(leftPanel, "By Vendor", true)
    byVendorBtn:SetSize(70, ACQ.VIEW_TOGGLE_HEIGHT)
    byVendorBtn:SetPoint("TOPLEFT", 6, viewRowY)
    byVendorBtn:SetScript("OnClick", function()
        viewMode = "vendor"
        byVendorBtn:SetActive(true)
        acquisitionPanel.byItemBtn:SetActive(false)
        HDG_AcquisitionTab:RefreshList()
    end)
    acquisitionPanel.byVendorBtn = byVendorBtn

    local byItemBtn = HDG.UI:TabButton(leftPanel, "By Item", false)
    byItemBtn:SetSize(60, ACQ.VIEW_TOGGLE_HEIGHT)
    byItemBtn:SetPoint("LEFT", byVendorBtn, "RIGHT", 2, 0)
    byItemBtn:SetScript("OnClick", function()
        viewMode = "item"
        byItemBtn:SetActive(true)
        acquisitionPanel.byVendorBtn:SetActive(false)
        HDG_AcquisitionTab:RefreshList()
    end)
    acquisitionPanel.byItemBtn = byItemBtn

    -- Search box
    local searchBox = CreateFrame("EditBox", nil, leftPanel, "SearchBoxTemplate")
    searchBox:SetPoint("LEFT", byItemBtn, "RIGHT", 6, 0)
    searchBox:SetPoint("RIGHT", leftPanel, "RIGHT", -64, 0)
    searchBox:SetHeight(ACQ.VIEW_TOGGLE_HEIGHT)
    searchBox:SetAutoFocus(false)
    searchBox:SetScript("OnTextChanged", function(self)
        SearchBoxTemplate_OnTextChanged(self)
        HDG_AcqData.filters.search = self:GetText():lower()
        if searchDebounceTimer then searchDebounceTimer:Cancel() end
        searchDebounceTimer = C_Timer.NewTimer(ACQ.SEARCH_DEBOUNCE, function()
            HDG_AcquisitionTab:RefreshList()
        end)
    end)
    RegisterWidget(searchBox, "SearchBox")

    -- Map All button
    local mapAllBtn = HDG.UI:Button(leftPanel, "Map All", 52, ACQ.VIEW_TOGGLE_HEIGHT, function()
        if InCombatLockdown() then
            print("|cFFFF0000[HDG]|r Cannot set waypoints during combat.")
            return
        end
        if not TomTom or not TomTom.AddWaypoint then
            print("|cFFFFAA00[HDG]|r Install TomTom for waypoint support.")
            return
        end
        local seen = {}
        local count = 0
        local filters = HDG_AcqData.filters
        local function tryMapVendor(npcID)
            if seen[npcID] then return 0 end
            local ve = HDG_VendorDB and HDG_VendorDB[npcID]
            if not ve or not ve[4] or not ve[5] or (ve[4] == 0 and ve[5] == 0)
                or ve[4] < -5 or ve[4] > 105 or ve[5] < -5 or ve[5] > 105 then return 0 end
            if filters.expansion ~= "All" and HDG_AcqData.vendorExpansion[npcID] ~= filters.expansion then return 0 end
            if filters.zone ~= "All" and HDG_AcqData.GetVendorZone(npcID) ~= filters.zone then return 0 end
            seen[npcID] = true
            local uiMapID, mapX, mapY = ZonePctToMap(ve[3] or 0, ve[4], ve[5])
            if not uiMapID or not mapX or not mapY then return 0 end
            local ok = pcall(function()
                TomTom:AddWaypoint(uiMapID, mapX, mapY, {
                    title = ve[1] or ("NPC #" .. npcID),
                    persistent = false,
                    from = "HDG",
                })
            end)
            return ok and 1 or 0
        end
        for _, itemID in ipairs(HDG_AcqData.filteredItems) do
            local npcIDs = HDG_VendorByItem and HDG_VendorByItem[itemID]
            if npcIDs then
                for _, npcID in ipairs(npcIDs) do
                    count = count + tryMapVendor(npcID)
                end
            end
        end
        if count > 0 then
            print(string.format("|cFF00FF00[HDG]|r Set %d TomTom waypoints for filtered vendors.", count))
        else
            print("|cFFFFAA00[HDG]|r No valid vendor locations to map.")
        end
    end)
    mapAllBtn:SetPoint("TOPRIGHT", leftPanel, "TOPRIGHT", -6, viewRowY)

    -- ========================================================================
    -- ROW 2: EXPANSION / ZONE / FACTION DROPDOWNS
    -- ========================================================================

    local labelY = viewRowY - ACQ.VIEW_TOGGLE_HEIGHT - 4

    local expLabel = leftPanel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    expLabel:SetPoint("TOPLEFT", 6, labelY)
    if scheme then
        ApplyFont(expLabel, scheme, "small")
        expLabel:SetTextColor(scheme.accent.r, scheme.accent.g, scheme.accent.b, scheme.accent.a)
    end
    expLabel:SetText("Expansion")

    local zoneLabel = leftPanel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    zoneLabel:SetPoint("LEFT", expLabel, "LEFT", 104, 0)
    if scheme then
        ApplyFont(zoneLabel, scheme, "small")
        zoneLabel:SetTextColor(scheme.accent.r, scheme.accent.g, scheme.accent.b, scheme.accent.a)
    end
    zoneLabel:SetText("Zone")

    local factionLabel = leftPanel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    factionLabel:SetPoint("LEFT", zoneLabel, "LEFT", 114, 0)
    if scheme then
        ApplyFont(factionLabel, scheme, "small")
        factionLabel:SetTextColor(scheme.accent.r, scheme.accent.g, scheme.accent.b, scheme.accent.a)
    end
    factionLabel:SetText("Reputation")

    local row2Y = labelY - 12

    local expansionDropdown = HDG.UI:Dropdown(leftPanel, {
        items = HDG_AcqData.uniqueExpansions,
        currentValue = "All",
        width = 100,
        colorFn = function(val)
            if val == "All" then return nil end
            return HDG_ExpansionData.GetColor(val)
        end,
        onChange = function(val)
            HDG_AcqData.filters.expansion = val
            HDG_AcquisitionTab:RefreshList()
        end,
    })
    expansionDropdown:SetPoint("TOPLEFT", 6, row2Y)
    acquisitionPanel.expansionDropdown = expansionDropdown

    local function activeFilterColor(val)
        if val == "All" then return nil end
        local s = GetScheme()
        return s and { s.accent.r, s.accent.g, s.accent.b }
    end

    local zoneDropdown = HDG.UI:Dropdown(leftPanel, {
        items = HDG_AcqData.uniqueZones,
        currentValue = "All",
        width = 110,
        colorFn = activeFilterColor,
        onChange = function(val)
            HDG_AcqData.filters.zone = val
            HDG_AcquisitionTab:RefreshList()
        end,
    })
    zoneDropdown:SetPoint("LEFT", expansionDropdown, "RIGHT", 4, 0)
    acquisitionPanel.zoneDropdown = zoneDropdown

    local factionDropdown = HDG.UI:Dropdown(leftPanel, {
        items = HDG_AcqData.uniqueFactions,
        currentValue = "All",
        width = 110,
        colorFn = activeFilterColor,
        onChange = function(val)
            HDG_AcqData.filters.faction = val
            HDG_AcquisitionTab:RefreshList()
        end,
    })
    factionDropdown:SetPoint("LEFT", zoneDropdown, "RIGHT", 4, 0)
    acquisitionPanel.factionDropdown = factionDropdown

    -- ========================================================================
    -- ROW 3: UNCOLLECTED + SOURCE DROPDOWN
    -- ========================================================================

    local row3Y = row2Y - 24

    local sourceLabel = leftPanel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    sourceLabel:SetPoint("TOPLEFT", 6, row3Y - 4)
    if scheme then
        ApplyFont(sourceLabel, scheme, "small")
        sourceLabel:SetTextColor(scheme.accent.r, scheme.accent.g, scheme.accent.b, scheme.accent.a)
    end
    sourceLabel:SetText("Source")

    local sourceDropdown = HDG.UI:Dropdown(leftPanel, {
        items = {"Vendor", "Achievement", "Quest", "Drop", "Crafted", "Treasure", "Promotion", "Reputation", "Endeavor"},
        currentValue = "All",
        width = 100,
        colorFn = activeFilterColor,
        onChange = function(val)
            HDG_AcqData.filters.source = val
            HDG_AcquisitionTab:RefreshList()
        end,
    })
    sourceDropdown:SetPoint("LEFT", sourceLabel, "RIGHT", 7, 0)
    acquisitionPanel.sourceDropdown = sourceDropdown

    local uncollectedCheck = HDG.UI:Checkbox(leftPanel, 14, "Uncollected")
    uncollectedCheck:SetPoint("LEFT", sourceDropdown, "RIGHT", 8, 0)
    uncollectedCheck.onChanged = function(checked)
        HDG_AcqData.filters.uncollectedOnly = checked
        HDG_AcquisitionTab:RefreshList()
    end

    local dyeableCheck = HDG.UI:Checkbox(leftPanel, 14, "Dye")
    dyeableCheck:SetPoint("LEFT", uncollectedCheck, "LEFT", 96, 0)
    dyeableCheck.onChanged = function(checked)
        HDG_AcqData.filters.dyeableOnly = checked
        HDG_AcquisitionTab:RefreshList()
    end

    -- Item list scroll frame
    local listScrollFrame = CreateFrame("ScrollFrame", nil, leftPanel, "UIPanelScrollFrameTemplate")
    listScrollFrame:SetPoint("TOPLEFT", 4, row3Y - 24)
    listScrollFrame:SetPoint("BOTTOMRIGHT", -20, 24)
    HDG_UIHelpers.StyleScrollBar(listScrollFrame)

    local listContent = CreateFrame("Frame", nil, listScrollFrame)
    listContent:SetWidth(leftWidth - 28)
    listContent:SetHeight(1)
    listScrollFrame:SetScrollChild(listContent)
    acquisitionPanel.listContent = listContent
    acquisitionPanel.listScrollFrame = listScrollFrame
    RegisterWidget(listScrollFrame, "ScrollFrame")

    -- Virtual scroll hook — fires on all scroll events, checks flag before acting
    listScrollFrame:HookScript("OnVerticalScroll", function(_, offset)
        if virtualScrollActive then
            if virtualScrollDebounce then virtualScrollDebounce:Cancel() end
            virtualScrollDebounce = C_Timer.NewTimer(0.02, function()
                HDG_AcquisitionTab:UpdateVisibleItemRows(offset)
            end)
        end
    end)

    -- Status bar at bottom of left panel
    local statusBar = leftPanel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    statusBar:SetPoint("BOTTOMLEFT", 6, 6)
    statusBar:SetPoint("BOTTOMRIGHT", -6, 6)
    statusBar:SetJustifyH("LEFT")
    if scheme then
        ApplyFont(statusBar, scheme, "small")
        statusBar:SetTextColor(scheme.text_dim.r, scheme.text_dim.g, scheme.text_dim.b, scheme.text_dim.a)
    end
    acquisitionPanel.statusBar = statusBar

    -- ========================================================================
    -- RIGHT PANEL — Unified 3-section layout
    -- ========================================================================

    local rightPanel = HDG.UI:Panel(acquisitionPanel, { width = rightWidth })
    rightPanel:SetPoint("TOPLEFT", leftPanel, "TOPRIGHT", 2, 0)
    rightPanel:SetPoint("BOTTOMRIGHT", -1, 1)
    acquisitionPanel.rightPanel = rightPanel

    -- ---- Top: headerPanel (fixed height) ----
    local headerPanel = HDG.UI:Panel(rightPanel)
    headerPanel:SetPoint("TOPLEFT", 4, -4)
    headerPanel:SetPoint("TOPRIGHT", -4, -4)
    headerPanel:SetHeight(ACQ.HEADER_PANEL_HEIGHT)
    headerPanel:Hide()
    acquisitionPanel.headerPanel = headerPanel

    headerPanel.itemName = headerPanel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    headerPanel.itemName:SetPoint("TOPLEFT", 8, -6)
    headerPanel.itemName:SetPoint("RIGHT", headerPanel, "RIGHT", -110, 0)
    headerPanel.itemName:SetWordWrap(true)
    headerPanel.itemName:SetJustifyH("LEFT")
    if scheme then
        ApplyFont(headerPanel.itemName, scheme, "header")
        headerPanel.itemName:SetTextColor(scheme.text_header.r, scheme.text_header.g, scheme.text_header.b, scheme.text_header.a)
    end

    headerPanel.subtitle = headerPanel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    headerPanel.subtitle:SetPoint("TOPLEFT", headerPanel.itemName, "BOTTOMLEFT", 0, -2)
    headerPanel.subtitle:SetPoint("RIGHT", headerPanel, "RIGHT", -110, 0)
    headerPanel.subtitle:SetJustifyH("LEFT")
    if scheme then
        ApplyFont(headerPanel.subtitle, scheme, "small")
    end
    headerPanel.subtitle:Hide()

    headerPanel.collectedBadge = headerPanel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    headerPanel.collectedBadge:SetPoint("RIGHT", headerPanel, "RIGHT", -8, 0)
    headerPanel.collectedBadge:SetJustifyH("RIGHT")
    if scheme then
        ApplyFont(headerPanel.collectedBadge, scheme, "small")
    end

    -- Vendor note pencil icon + text (anchored at bottom of header)
    local vendorNoteBtnH = CreateFrame("Button", nil, headerPanel)
    vendorNoteBtnH:SetSize(12, 12)
    vendorNoteBtnH:SetPoint("BOTTOMLEFT", 8, 4)
    vendorNoteBtnH:SetNormalAtlas("communities-icon-notification")
    vendorNoteBtnH:SetHighlightAtlas("communities-icon-notification")
    vendorNoteBtnH:SetScript("OnClick", function()
        if selectedVendor and selectedVendor.npcID then
            local vName = selectedVendor.name or ("NPC #" .. selectedVendor.npcID)
            StaticPopup_Show("HDG_EDIT_VENDOR_NOTE", vName, nil, selectedVendor.npcID)
        end
    end)
    HDG_UIHelpers.SetupTooltip(vendorNoteBtnH, "Vendor Note", "Click to add or edit a note for this vendor.", "ANCHOR_RIGHT")
    headerPanel.vendorNoteBtn = vendorNoteBtnH

    headerPanel.vendorNoteText = headerPanel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    headerPanel.vendorNoteText:SetPoint("LEFT", vendorNoteBtnH, "RIGHT", 4, 0)
    headerPanel.vendorNoteText:SetPoint("RIGHT", headerPanel, "RIGHT", -60, 0)
    headerPanel.vendorNoteText:SetWordWrap(false)
    headerPanel.vendorNoteText:SetJustifyH("LEFT")
    if scheme then ApplyFont(headerPanel.vendorNoteText, scheme, "small") end
    headerPanel.vendorNoteText:SetText(C_Dim() .. "Add vendor note...|r")

    -- Make vendor note text clickable
    local vendorNoteClickZone = CreateFrame("Button", nil, headerPanel)
    vendorNoteClickZone:SetPoint("TOPLEFT", headerPanel.vendorNoteText, "TOPLEFT", 0, 2)
    vendorNoteClickZone:SetPoint("BOTTOMRIGHT", headerPanel.vendorNoteText, "BOTTOMRIGHT", 0, -2)
    vendorNoteClickZone:SetScript("OnClick", function()
        if selectedVendor and selectedVendor.npcID then
            local vName = selectedVendor.name or ("NPC #" .. selectedVendor.npcID)
            StaticPopup_Show("HDG_EDIT_VENDOR_NOTE", vName, nil, selectedVendor.npcID)
        end
    end)
    headerPanel.vendorNoteClickZone = vendorNoteClickZone

    -- ---- Middle: detailPanel (flexible height) ----
    local detailPanel = HDG.UI:Panel(rightPanel)
    detailPanel:SetPoint("TOPLEFT", headerPanel, "BOTTOMLEFT", 0, -2)
    detailPanel:SetPoint("RIGHT", rightPanel, "RIGHT", -4, 0)
    detailPanel:SetPoint("BOTTOM", rightPanel, "BOTTOM", 0, ACQ.GALLERY_HEIGHT + 24)
    detailPanel:Hide()
    acquisitionPanel.detailPanel = detailPanel

    -- ---- Preview frame (left sub-frame, square, fills height) ----
    local previewFrame = HDG.UI:Panel(detailPanel)
    previewFrame:SetPoint("TOPLEFT", 4, -4)
    previewFrame:SetPoint("BOTTOMLEFT", 4, 4)
    previewFrame:SetWidth(ACQ.SELECTED_PREVIEW_SIZE)
    previewFrame:SetScript("OnSizeChanged", function(self, w, h)
        if h > 0 and math.abs(w - h) > 1 then self:SetWidth(h) end
    end)
    detailPanel.previewBg = previewFrame

    local previewScene = CreateFrame("ModelScene", nil, previewFrame, "PanningModelSceneMixinTemplate")
    previewScene:SetPoint("TOPLEFT", 2, -2)
    previewScene:SetPoint("BOTTOMRIGHT", -2, 2)
    previewScene:EnableMouse(true)
    previewScene:EnableMouseWheel(true)

    local rotating = false
    local lastX, lastY = 0, 0
    previewScene:SetScript("OnMouseDown", function(self, button)
        if button == "LeftButton" then rotating = true; lastX, lastY = GetCursorPosition() end
    end)
    previewScene:SetScript("OnMouseUp", function(self, button)
        if button == "LeftButton" then rotating = false end
    end)
    previewScene:SetScript("OnUpdate", function(self)
        if rotating then
            local x, y = GetCursorPosition()
            local actor = self:GetActorByTag("decor")
            if actor then
                actor:SetYaw((actor:GetYaw() or 0) + (x - lastX) * 0.01)
                actor:SetPitch((actor:GetPitch() or 0) + (y - lastY) * 0.01)
            end
            lastX, lastY = x, y
        end
    end)
    previewScene:SetScript("OnMouseWheel", function(self, delta)
        local actor = self:GetActorByTag("decor")
        if actor then
            local scale = actor:GetScale() or 1
            scale = math.max(0.1, math.min(5.0, scale * (delta > 0 and 1.1 or 0.9)))
            actor:SetScale(scale)
        end
    end)
    previewScene:Hide()
    detailPanel.previewScene = previewScene

    -- ---- Info frame (right sub-frame, text lines + other vendors) ----
    local infoFrame = HDG.UI:Panel(detailPanel)
    infoFrame:SetPoint("TOPLEFT", previewFrame, "TOPRIGHT", 4, 0)
    infoFrame:SetPoint("BOTTOMRIGHT", -4, 4)
    detailPanel.infoFrame = infoFrame

    local function MakeDetailLine(parent, anchorTo, anchorPoint, yOff)
        local fs = parent:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        if anchorPoint == "TOPLEFT" then
            fs:SetPoint("TOPLEFT", anchorTo, "BOTTOMLEFT", 0, yOff or -2)
        else
            fs:SetPoint("TOPLEFT", anchorTo, "TOPRIGHT", 8, yOff or -2)
        end
        fs:SetPoint("RIGHT", parent, "RIGHT", -8, 0)
        fs:SetWordWrap(true)
        fs:SetJustifyH("LEFT")
        if scheme then ApplyFont(fs, scheme, "small") end
        return fs
    end

    detailPanel.line1 = infoFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    detailPanel.line1:SetPoint("TOPLEFT", 8, -6)
    detailPanel.line1:SetPoint("RIGHT", infoFrame, "RIGHT", -8, 0)
    detailPanel.line1:SetWordWrap(true)
    detailPanel.line1:SetJustifyH("LEFT")
    if scheme then ApplyFont(detailPanel.line1, scheme, "small") end

    detailPanel.line2 = MakeDetailLine(infoFrame, detailPanel.line1, "TOPLEFT")
    detailPanel.line3 = MakeDetailLine(infoFrame, detailPanel.line2, "TOPLEFT")
    detailPanel.line4 = MakeDetailLine(infoFrame, detailPanel.line3, "TOPLEFT")
    detailPanel.line5 = MakeDetailLine(infoFrame, detailPanel.line4, "TOPLEFT")
    detailPanel.line6 = MakeDetailLine(infoFrame, detailPanel.line5, "TOPLEFT")
    detailPanel.line7 = MakeDetailLine(infoFrame, detailPanel.line6, "TOPLEFT")
    detailPanel.line8 = MakeDetailLine(infoFrame, detailPanel.line7, "TOPLEFT")
    detailPanel.line9 = MakeDetailLine(infoFrame, detailPanel.line8, "TOPLEFT")

    -- Other-vendors label + clickable button pool
    detailPanel.otherVendorsLabel = infoFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    detailPanel.otherVendorsLabel:SetPoint("TOPLEFT", detailPanel.line9, "BOTTOMLEFT", 0, -6)
    detailPanel.otherVendorsLabel:SetPoint("RIGHT", infoFrame, "RIGHT", -8, 0)
    detailPanel.otherVendorsLabel:SetWordWrap(true)
    detailPanel.otherVendorsLabel:SetJustifyH("LEFT")
    if scheme then
        ApplyFont(detailPanel.otherVendorsLabel, scheme, "small")
        detailPanel.otherVendorsLabel:SetTextColor(scheme.text_dim.r, scheme.text_dim.g, scheme.text_dim.b, scheme.text_dim.a)
    end

    -- Item note display (pencil icon + text below other vendors)
    local itemNoteBtn = CreateFrame("Button", nil, infoFrame)
    itemNoteBtn:SetSize(12, 12)
    itemNoteBtn:SetPoint("TOPLEFT", detailPanel.otherVendorsLabel, "BOTTOMLEFT", 0, -6)
    itemNoteBtn:SetNormalAtlas("communities-icon-notification")
    itemNoteBtn:SetHighlightAtlas("communities-icon-notification")
    itemNoteBtn:SetScript("OnClick", function()
        if selectedItemID then
            local iName = HDG_Data.GetCachedItemName(selectedItemID) or ("Item #" .. selectedItemID)
            StaticPopup_Show("HDG_EDIT_USER_NOTE", iName, nil, selectedItemID)
        end
    end)
    HDG_UIHelpers.SetupTooltip(itemNoteBtn, "Item Note", "Click to add or edit a personal note for this item.", "ANCHOR_RIGHT")
    detailPanel.itemNoteBtn = itemNoteBtn

    detailPanel.itemNoteText = infoFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    detailPanel.itemNoteText:SetPoint("LEFT", itemNoteBtn, "RIGHT", 4, 0)
    detailPanel.itemNoteText:SetPoint("RIGHT", infoFrame, "RIGHT", -8, 0)
    detailPanel.itemNoteText:SetWordWrap(false)
    detailPanel.itemNoteText:SetJustifyH("LEFT")
    if scheme then ApplyFont(detailPanel.itemNoteText, scheme, "small") end

    -- Waypoint / Map buttons
    local waypointBtn = HDG.UI:Button(rightPanel, "Set Waypoint", ACQ.WAYPOINT_BTN_WIDTH, ACQ.WAYPOINT_BTN_HEIGHT, function()
        if not selectedVendor then return end
        if InCombatLockdown() then
            print("|cFFFF0000[HDG]|r Cannot set waypoint during combat.")
            return
        end
        if selectedVendor.mapID == 0 or (selectedVendor.x == 0 and selectedVendor.y == 0) then
            print("|cFFFFAA00[HDG]|r No valid coordinates for this vendor.")
            return
        end
        if TomTom and TomTom.AddWaypoint then
            local uiMapID, mapX, mapY = ZonePctToMap(selectedVendor.mapID, selectedVendor.x, selectedVendor.y)
            if uiMapID and mapX and mapY then
                local ok, err = pcall(function()
                    local uid = TomTom:AddWaypoint(uiMapID, mapX, mapY, {
                        title = selectedVendor.name,
                        persistent = false,
                        crazy = true,
                        from = "HDG",
                    })
                    if uid then TomTom:SetCrazyArrow(uid, TomTom.profile.arrow.arrival or 5, selectedVendor.name) end
                end)
                if ok then
                    print("|cFF00FF00[HDG]|r TomTom waypoint set for " .. selectedVendor.name)
                else
                    print("|cFFFFAA00[HDG]|r Could not set waypoint — " .. tostring(err))
                end
            else
                print("|cFFFFAA00[HDG]|r Waypoint not available for zone: " .. tostring(selectedVendor.zone))
            end
        else
            print("|cFFFFAA00[HDG]|r Install TomTom for waypoint support.")
        end
    end)
    waypointBtn:SetPoint("TOPRIGHT", headerPanel, "TOPRIGHT", -6, -6)
    waypointBtn:Hide()
    acquisitionPanel.waypointBtn = waypointBtn

    local mapBtn = HDG.UI:Button(rightPanel, "Show on Map", ACQ.WAYPOINT_BTN_WIDTH, ACQ.WAYPOINT_BTN_HEIGHT, function()
        if not selectedVendor then return end
        if InCombatLockdown() then
            print("|cFFFF0000[HDG]|r Cannot open map during combat.")
            return
        end
        if selectedVendor.mapID == 0 or (selectedVendor.x == 0 and selectedVendor.y == 0) then
            print("|cFFFFAA00[HDG]|r No valid coordinates for this vendor.")
            return
        end
        local uiMapID, mapX, mapY = ZonePctToMap(selectedVendor.mapID, selectedVendor.x, selectedVendor.y)
        if uiMapID and mapX and mapY and C_Map.GetMapInfo(uiMapID) then
            local ok = pcall(function()
                C_Map.SetUserWaypoint(UiMapPoint.CreateFromCoordinates(uiMapID, mapX, mapY))
                C_SuperTrack.SetSuperTrackedUserWaypoint(true)
                OpenWorldMap(uiMapID)
            end)
            if not ok then
                print("|cFFFFAA00[HDG]|r Could not set map pin for this zone.")
            end
        else
            print("|cFFFFAA00[HDG]|r Map pin not available for zone: " .. tostring(selectedVendor.zone))
        end
    end)
    mapBtn:SetPoint("TOPRIGHT", waypointBtn, "BOTTOMRIGHT", 0, -2)
    mapBtn:Hide()
    acquisitionPanel.mapBtn = mapBtn

    -- ---- Bottom (A): galleryPanel — vendor items gallery ----
    local galleryPanel = CreateFrame("Frame", nil, rightPanel)
    galleryPanel:SetPoint("BOTTOMLEFT", 0, 0)
    galleryPanel:SetPoint("BOTTOMRIGHT", 0, 0)
    galleryPanel:SetHeight(ACQ.GALLERY_HEIGHT + 22)
    galleryPanel:Hide()
    acquisitionPanel.galleryPanel = galleryPanel

    local galleryHeader = galleryPanel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    galleryHeader:SetPoint("TOPLEFT", galleryPanel, "TOPLEFT", 8, -2)
    if scheme then
        ApplyFont(galleryHeader, scheme, "body")
        galleryHeader:SetTextColor(scheme.text_header.r, scheme.text_header.g, scheme.text_header.b, scheme.text_header.a)
    end
    galleryHeader:SetText("")
    acquisitionPanel.galleryHeader = galleryHeader

    local costSummary = galleryPanel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    costSummary:SetPoint("LEFT", galleryHeader, "RIGHT", 8, 0)
    if scheme then
        ApplyFont(costSummary, scheme, "small")
        costSummary:SetTextColor(scheme.text_dim.r, scheme.text_dim.g, scheme.text_dim.b, scheme.text_dim.a)
    end
    costSummary:SetText("")
    acquisitionPanel.costSummary = costSummary

    local galleryScroll = CreateFrame("ScrollFrame", nil, galleryPanel, "UIPanelScrollFrameTemplate")
    galleryScroll:SetPoint("TOPLEFT", galleryPanel, "TOPLEFT", 4, -18)
    galleryScroll:SetPoint("BOTTOMRIGHT", galleryPanel, "BOTTOMRIGHT", -20, 4)
    HDG_UIHelpers.StyleScrollBar(galleryScroll)

    local galleryContent = CreateFrame("Frame", nil, galleryScroll)
    galleryContent:SetWidth(rightWidth - 28)
    galleryContent:SetHeight(1)
    galleryScroll:SetScrollChild(galleryContent)
    acquisitionPanel.galleryContent = galleryContent
    acquisitionPanel.galleryScroll = galleryScroll
    RegisterWidget(galleryScroll, "ScrollFrame")

    -- ---- Bottom (B): infoPanel — non-vendor item info ----
    local infoPanel = HDG.UI:Panel(rightPanel)
    infoPanel:SetPoint("BOTTOMLEFT", 0, 0)
    infoPanel:SetPoint("BOTTOMRIGHT", 0, 0)
    infoPanel:SetHeight(ACQ.GALLERY_HEIGHT + 22)
    infoPanel:Hide()
    acquisitionPanel.infoPanel = infoPanel

    infoPanel.sourceLine = infoPanel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    infoPanel.sourceLine:SetPoint("TOPLEFT", 8, -8)
    infoPanel.sourceLine:SetPoint("RIGHT", infoPanel, "RIGHT", -8, 0)
    infoPanel.sourceLine:SetWordWrap(true)
    infoPanel.sourceLine:SetJustifyH("LEFT")
    if scheme then ApplyFont(infoPanel.sourceLine, scheme, "small") end

    infoPanel.expansionLine = infoPanel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    infoPanel.expansionLine:SetPoint("TOPLEFT", infoPanel.sourceLine, "BOTTOMLEFT", 0, -4)
    infoPanel.expansionLine:SetPoint("RIGHT", infoPanel, "RIGHT", -8, 0)
    infoPanel.expansionLine:SetWordWrap(true)
    infoPanel.expansionLine:SetJustifyH("LEFT")
    if scheme then ApplyFont(infoPanel.expansionLine, scheme, "small") end

    infoPanel.extraLine = infoPanel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    infoPanel.extraLine:SetPoint("TOPLEFT", infoPanel.expansionLine, "BOTTOMLEFT", 0, -4)
    infoPanel.extraLine:SetPoint("RIGHT", infoPanel, "RIGHT", -8, 0)
    infoPanel.extraLine:SetWordWrap(true)
    infoPanel.extraLine:SetJustifyH("LEFT")
    if scheme then ApplyFont(infoPanel.extraLine, scheme, "small") end

    -- Placeholder guide panel (scrollable, shown before any item is selected)
    local placeholder = CreateFrame("Frame", nil, rightPanel)
    placeholder:SetAllPoints()

    local guideScroll = CreateFrame("ScrollFrame", nil, placeholder, "UIPanelScrollFrameTemplate")
    guideScroll:SetPoint("TOPLEFT", 0, -4)
    guideScroll:SetPoint("BOTTOMRIGHT", -20, 4)
    HDG_UIHelpers.StyleScrollBar(guideScroll)
    RegisterWidget(guideScroll, "ScrollFrame")

    local guideContent = CreateFrame("Frame", nil, guideScroll)
    guideContent:SetWidth(rightWidth - 28)
    guideContent:SetHeight(1)
    guideScroll:SetScrollChild(guideContent)

    local guideTitle = guideContent:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    guideTitle:SetPoint("TOPLEFT", 8, -4)
    guideTitle:SetPoint("RIGHT", guideContent, "RIGHT", -8, 0)
    guideTitle:SetJustifyH("LEFT")
    if scheme then
        ApplyFont(guideTitle, scheme, "header")
        guideTitle:SetTextColor(scheme.text_header.r, scheme.text_header.g, scheme.text_header.b, scheme.text_header.a)
    end
    guideTitle:SetText("Acquisition Guide")

    local guideBody = guideContent:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    guideBody:SetPoint("TOPLEFT", guideTitle, "BOTTOMLEFT", 0, -8)
    guideBody:SetPoint("RIGHT", guideContent, "RIGHT", -8, 0)
    guideBody:SetJustifyH("LEFT")
    guideBody:SetWordWrap(true)
    guideBody:SetSpacing(3)
    if scheme then
        ApplyFont(guideBody, scheme, "small")
        guideBody:SetTextColor(scheme.text.r, scheme.text.g, scheme.text.b, scheme.text.a)
    end

    -- Store refs for dynamic rebuild
    placeholder.guideTitle = guideTitle
    placeholder.guideBody = guideBody
    placeholder.guideContent = guideContent

    acquisitionPanel.placeholder = placeholder

    return acquisitionPanel
end

-- ============================================================================
-- REFRESH ITEM LIST (By Item view)
-- ============================================================================

-- Rebuild guide text with current theme colors (called each time guide is shown)
-- NOTE: Uses SetTextColor for default text; only accent |c codes for highlights.
-- WoW FontStrings have a limit on inline color codes — minimizing |c usage is critical.
RefreshGuideText = function()
    local ph = acquisitionPanel.placeholder
    if not ph or not ph.guideBody then return end
    local a = C_Accent()
    local ico = function(atlas) return CreateAtlasMarkup(atlas, 14, 14) end
    local chk = "|TInterface\\RAIDFRAME\\ReadyCheck-Ready:14:14|t"
    local achv = ico("UI-HUD-MicroMenu-Achievements-Mouseover")
    local anvil = string.format("|T%s:14:14|t", CONFIG.TEXTURES.CRAFTED_ICON)

    -- |r resets to the FontString's SetTextColor (text color), so no need for t prefix
    local guideText = table.concat({
        a .. "Getting Started|r",
        "1. Pick a view on the left: " .. a .. "By Vendor|r or " .. a .. "By Item|r.",
        "2. Use the filters to narrow down what you're looking for.",
        "3. Click any item to see its details here.",
        "",
        a .. "Common Use Cases|r",
        "1. Find what you're missing — Check " .. a .. "Uncollected|r, switch to " .. a .. "By Item|r, and use " .. a .. "Search|r to find items by name.",
        "2. Plan a shopping trip — Pick an " .. a .. "Expansion|r, check " .. a .. "Uncollected|r, then hit " .. a .. "Map All|r to set waypoints for every vendor you need.",
        "3. Check rep-gated items — Set " .. a .. "Source|r to " .. a .. "Reputation|r to see which items need rep.",
        "4. Queue crafted items — Set " .. a .. "Source|r to " .. a .. "Crafted|r, select an item, then click " .. a .. "Add to Crafting Queue|r to send it to the Recipes tab.",
        "5. Preview before you buy — Click any item to see its 3D model, then use " .. a .. "Set Waypoint|r or " .. a .. "Show on Map|r to navigate to the vendor.",
        "",
        a .. "Views|r",
        a .. "By Vendor|r — Browse vendors and expand them to see their stock. Each row shows how many items you've collected (e.g. 3/5).",
        a .. "By Item|r — A flat alphabetical list of every acquirable item. Best for searching across all sources at once.",
        "",
        a .. "Filters|r",
        a .. "Expansion|r — Show only items from a specific expansion.",
        a .. "Zone|r — Limit to vendors in a specific zone.",
        a .. "Reputation|r — Show items requiring a specific faction.",
        a .. "Source|r — Filter by acquisition method: Vendor, Quest, Achievement, Drop, Crafted, Treasure, or Promotion.",
        a .. "Uncollected|r — Hide items you already own.",
        "",
        a .. "Map All|r (requires TomTom)",
        "Drops a waypoint on every vendor matching your filters. Combine with " .. a .. "Expansion|r or " .. a .. "Zone|r filters to plan a focused shopping route through one area.",
        "",
        a .. "Row Icons|r",
        chk .. " Collected — You already own this item.",
        ico("gficon-chest-evergreen-greatvault-collect") .. " Rep Met — You have enough reputation to buy this item.",
        ico("gficon-chest-evergreen-greatvault-incomplete") .. " Rep Not Met — You need more reputation with this faction.",
        achv .. " " .. a .. "Done|r (green) — Achievement completed, or item collected from a non-vendor source.",
        achv .. " " .. a .. "Incomplete|r (orange) — Achievement or non-vendor source not yet completed.",
        ico("communities-create-button-wow-alliance") .. " Alliance — This vendor is Alliance-only.",
        ico("communities-create-button-wow-horde") .. " Horde — This vendor is Horde-only.",
        "",
        a .. "Source Icons|r",
        achv .. " Achievement — Earned from an achievement.",
        ico("QuestNormal") .. " Quest / World Quest — Rewarded from a quest.",
        ico("Crosshair_lootall_32") .. " Drop — Looted from a creature or boss.",
        anvil .. " Crafted — Made via a profession recipe.",
        ico("VignetteLoot") .. " Treasure — Found in the world.",
        ico("Professions-Crafting-Orders-Icon") .. " Promotion — From a special event or promotion.",
    }, "\n")

    -- Apply current theme colors to fonts
    local scheme = HDG.Theme.currentScheme
    if scheme then
        ApplyFont(ph.guideTitle, scheme, "header")
        ph.guideTitle:SetTextColor(scheme.text_header.r, scheme.text_header.g, scheme.text_header.b, 1)
        ApplyFont(ph.guideBody, scheme, "small")
        ph.guideBody:SetTextColor(scheme.text.r, scheme.text.g, scheme.text.b, 1)
    end

    ph.guideBody:SetText(guideText)
    ph.guideContent:SetHeight(ph.guideTitle:GetStringHeight() + 8 + ph.guideBody:GetStringHeight() + 16)
end

-- Virtual scroll threshold — above this many items, only render visible rows
local VIRTUAL_SCROLL_THRESHOLD = 200

function HDG_AcquisitionTab:RefreshItemList()
    local content = acquisitionPanel.listContent
    local listWidth = content:GetWidth()
    if listWidth == 0 then listWidth = 200 end

    local ACQ = CONFIG.ACQUISITION_TAB
    local totalItems = #HDG_AcqData.filteredItems

    -- Count uncollected (always need full count for status bar)
    local uncollectedCount = 0
    for _, itemID in ipairs(HDG_AcqData.filteredItems) do
        if not HDG_Data.IsItemCollected(itemID) then
            uncollectedCount = uncollectedCount + 1
        end
    end

    if totalItems > VIRTUAL_SCROLL_THRESHOLD then
        -- Virtual scroll: only create rows for visible viewport
        virtualScrollActive = true
        virtualScrollFirstIdx = 0 -- force first render

        local scrollFrame = acquisitionPanel.listScrollFrame
        local visibleHeight = scrollFrame:GetHeight()
        if visibleHeight == 0 then visibleHeight = 400 end

        -- Set total content height (for scrollbar range)
        content:SetHeight(totalItems * ACQ.ITEM_ROW_SPACING + 10)

        -- Render initial visible set
        self:UpdateVisibleItemRows(0)
    else
        -- Small list: create all rows directly (existing behavior)
        virtualScrollActive = false
        local yOffset = 0
        local onSelect = function(itemID) HDG_AcquisitionTab:SelectItem(itemID) end

        for _, itemID in ipairs(HDG_AcqData.filteredItems) do
            HDG_AcqRows.CreateItemRow(content, itemID, yOffset, listWidth, nil, selectedItemID, onSelect, CONFIG)
            yOffset = yOffset - ACQ.ITEM_ROW_SPACING
        end
        content:SetHeight(math.abs(yOffset) + 10)
    end

    return uncollectedCount
end

-- Update visible rows for virtual scrolling (called on scroll and initial render)
function HDG_AcquisitionTab:UpdateVisibleItemRows(scrollOffset)
    local ACQ = CONFIG.ACQUISITION_TAB
    local content = acquisitionPanel.listContent
    local listWidth = content:GetWidth()
    if listWidth == 0 then listWidth = 200 end

    local scrollFrame = acquisitionPanel.listScrollFrame
    local visibleHeight = scrollFrame:GetHeight()
    if visibleHeight == 0 then visibleHeight = 400 end
    local maxVisible = math.ceil(visibleHeight / ACQ.ITEM_ROW_SPACING) + 4

    local newFirst = math.max(1, math.floor(scrollOffset / ACQ.ITEM_ROW_SPACING) + 1)
    if newFirst == virtualScrollFirstIdx then return end -- no change
    virtualScrollFirstIdx = newFirst

    -- Release existing item rows and recreate for new viewport
    HDG_AcqRows.ReleaseAllRows()

    local items = HDG_AcqData.filteredItems
    local lastIdx = math.min(newFirst + maxVisible - 1, #items)
    local onSelect = function(itemID) HDG_AcquisitionTab:SelectItem(itemID) end

    for i = newFirst, lastIdx do
        local itemID = items[i]
        local yOffset = -(i - 1) * ACQ.ITEM_ROW_SPACING
        HDG_AcqRows.CreateItemRow(content, itemID, yOffset, listWidth, nil, selectedItemID, onSelect, CONFIG)
    end
end

-- ============================================================================
-- REFRESH VENDOR LIST (By Vendor view)
-- ============================================================================

function HDG_AcquisitionTab:RefreshVendorList()
    local content = acquisitionPanel.listContent
    local listWidth = content:GetWidth()
    if listWidth == 0 then listWidth = 200 end

    local yOffset = 0
    local uncollectedCount = 0
    local ACQ = CONFIG.ACQUISITION_TAB

    HDG_AcqData.RebuildFilteredVendors(CONFIG)

    local onSelect = function(itemID) HDG_AcquisitionTab:SelectItem(itemID) end
    local onToggle = function(npcID)
        expandedVendors[npcID] = not expandedVendors[npcID]
        local ve = HDG_VendorDB and HDG_VendorDB[npcID]
        if ve then
            selectedVendor = {
                npcID = npcID,
                name = ve[1] or ("NPC #" .. npcID),
                zone = ve[2] or "Unknown",
                mapID = ve[3] or 0,
                x = ve[4] or 0,
                y = ve[5] or 0,
            }
        end
        HDG_AcquisitionTab:RefreshList(true)
        -- Auto-select first filtered item when expanding a vendor
        if expandedVendors[npcID] then
            local counts = HDG_AcqData.vendorCounts[npcID]
            local nm = counts and counts.nameMatch
            local savedS
            if nm then savedS = HDG_AcqData.filters.search; HDG_AcqData.filters.search = "" end
            local items = HDG_AcqData.vendorItems[npcID] or {}
            for _, itemID in ipairs(items) do
                if HDG_AcqData.ItemPassesFilters(itemID, CONFIG) then
                    if nm then HDG_AcqData.filters.search = savedS end
                    HDG_AcquisitionTab:SelectItem(itemID)
                    break
                end
            end
            if nm and HDG_AcqData.filters.search == "" then HDG_AcqData.filters.search = savedS end
        end
    end

    for _, npcID in ipairs(HDG_AcqData.filteredVendors) do
        HDG_AcqRows.CreateVendorHeaderRow(content, npcID, yOffset, listWidth, expandedVendors[npcID], onToggle, CONFIG)
        yOffset = yOffset - ACQ.VENDOR_ROW_HEIGHT

        if expandedVendors[npcID] then
            local items = HDG_AcqData.vendorItems[npcID] or {}
            local counts = HDG_AcqData.vendorCounts[npcID]
            local nameMatch = counts and counts.nameMatch
            -- If vendor matched by name search, temporarily clear search to show all its items
            local savedSearch
            if nameMatch then
                savedSearch = HDG_AcqData.filters.search
                HDG_AcqData.filters.search = ""
            end
            wipe(reusableItemList)
            for _, itemID in ipairs(items) do
                if HDG_AcqData.ItemPassesFilters(itemID, CONFIG) then
                    table.insert(reusableItemList, itemID)
                end
            end
            if nameMatch then HDG_AcqData.filters.search = savedSearch end
            table.sort(reusableItemList, sortByItemName)

            for _, itemID in ipairs(reusableItemList) do
                if not HDG_Data.IsItemCollected(itemID) then
                    uncollectedCount = uncollectedCount + 1
                end
                local iRow = HDG_AcqRows.CreateItemRow(content, itemID, yOffset, listWidth - ACQ.VENDOR_INDENT, ACQ.VENDOR_INDENT, selectedItemID, onSelect, CONFIG)
                if iRow then iRow.parentNpcID = npcID end
                yOffset = yOffset - ACQ.ITEM_ROW_SPACING
            end
        end
    end

    content:SetHeight(math.abs(yOffset) + 10)
    return uncollectedCount
end

-- ============================================================================
-- REFRESH LIST (dispatches by view mode)
-- ============================================================================

function HDG_AcquisitionTab:RefreshList(keepScroll)
    if not acquisitionPanel then return end

    virtualScrollActive = false -- disable until RefreshItemList re-enables if needed
    HDG_AcqRows.ReleaseAllRows()
    HDG_AcqData.RebuildFilteredItems(CONFIG)

    -- Pre-cache dyeable status for all filtered items (batch API calls)
    if HDG_DyeData and HDG_DyeData.PreCacheDyeableItems then
        HDG_DyeData.PreCacheDyeableItems(HDG_AcqData.filteredItems)
    end

    local uncollectedCount
    if viewMode == "vendor" then
        uncollectedCount = self:RefreshVendorList()
    else
        uncollectedCount = self:RefreshItemList()
    end

    -- Reset scroll position after content is rebuilt (skip when expanding/collapsing vendors)
    if not keepScroll and acquisitionPanel.listScrollFrame then
        acquisitionPanel.listScrollFrame:SetVerticalScroll(0)
    end

    -- Update status bar
    if acquisitionPanel.statusBar then
        if viewMode == "vendor" then
            acquisitionPanel.statusBar:SetText(string.format(
                "Showing %d vendors (%d items)", #HDG_AcqData.filteredVendors, #HDG_AcqData.filteredItems
            ))
        else
            acquisitionPanel.statusBar:SetText(string.format(
                "Showing %d items (%d uncollected)", #HDG_AcqData.filteredItems, uncollectedCount or 0
            ))
        end
    end

    -- Update header title with counts
    if acquisitionPanel.headerTitle then
        acquisitionPanel.headerTitle:SetText(string.format(
            "%sAcquisition Guide|r  %s(%d items)|r",
            C_Text(), C_Dim(), #HDG_AcqData.filteredItems
        ))
    end

    -- Update vendor shopping list badge
    if acquisitionPanel.shopListBtn and HDG_VendorShoppingList then
        local count = HDG_VendorShoppingList.GetCount()
        acquisitionPanel.shopListBtn:SetText(count > 0 and ("Shopping List - " .. count) or "Shopping List")
    end
end

-- ============================================================================
-- SELECT ITEM (updates all right panel sections)
-- ============================================================================

function HDG_AcquisitionTab:SelectItem(itemID)
    if not acquisitionPanel or not itemID then return end
    selectedItemID = itemID

    if acquisitionPanel.placeholder then acquisitionPanel.placeholder:Hide() end

    acquisitionPanel.headerPanel:Show()
    acquisitionPanel.detailPanel:Show()

    local vendors = HDG_VendorResolver.GetVendorsForItem(itemID)
    local hasVendors = vendors and #vendors > 0
    local isCrafted = HDG_AcqData.craftableByItem[itemID] ~= nil

    if isCrafted and not hasVendors then
        selectedVendor = nil
        galleryVendorNPC = nil
        acquisitionPanel.waypointBtn:Hide()
        acquisitionPanel.mapBtn:Hide()
        HDG_AcqRows.ReleaseAllCells()
        acquisitionPanel.detailPanel:ClearAllPoints()
        acquisitionPanel.detailPanel:SetPoint("TOPLEFT", acquisitionPanel.headerPanel, "BOTTOMLEFT", 0, -2)
        acquisitionPanel.detailPanel:SetPoint("RIGHT", acquisitionPanel.rightPanel, "RIGHT", -4, 0)
        acquisitionPanel.detailPanel:SetPoint("BOTTOM", acquisitionPanel.rightPanel, "BOTTOM", 0, CONFIG.ACQUISITION_TAB.GALLERY_HEIGHT + 24)
        acquisitionPanel.galleryPanel:Hide()
        acquisitionPanel.infoPanel:Show()
        self:UpdateHeaderPanel(itemID)
        self:UpdateDetailPanel_Crafted(itemID)
        self:UpdateInfoPanel_Crafted(itemID)
    elseif hasVendors then
        local newVendor = vendors[1]
        if selectedVendor then
            for _, v in ipairs(vendors) do
                if v.npcID == selectedVendor.npcID then
                    newVendor = v
                    break
                end
            end
        end
        local vendorChanged = galleryVendorNPC ~= newVendor.npcID
            or not acquisitionPanel.galleryPanel:IsShown()
        selectedVendor = newVendor
        if vendorChanged then
            galleryVendorNPC = newVendor.npcID
            self:UpdateVendorInventory(selectedVendor)
        else
            HDG_AcqRows.UpdateGalleryHighlights(selectedItemID)
        end
        acquisitionPanel.detailPanel:ClearAllPoints()
        acquisitionPanel.detailPanel:SetPoint("TOPLEFT", acquisitionPanel.headerPanel, "BOTTOMLEFT", 0, -2)
        acquisitionPanel.detailPanel:SetPoint("RIGHT", acquisitionPanel.rightPanel, "RIGHT", -4, 0)
        acquisitionPanel.detailPanel:SetPoint("BOTTOM", acquisitionPanel.rightPanel, "BOTTOM", 0, CONFIG.ACQUISITION_TAB.GALLERY_HEIGHT + 24)
        acquisitionPanel.galleryPanel:Show()
        acquisitionPanel.infoPanel:Hide()
        self:UpdateHeaderPanel(itemID)
        self:UpdateDetailPanel_Vendor(itemID, vendors)
    else
        selectedVendor = nil
        galleryVendorNPC = nil
        acquisitionPanel.waypointBtn:Hide()
        acquisitionPanel.mapBtn:Hide()
        HDG_AcqRows.ReleaseAllCells()
        acquisitionPanel.detailPanel:ClearAllPoints()
        acquisitionPanel.detailPanel:SetPoint("TOPLEFT", acquisitionPanel.headerPanel, "BOTTOMLEFT", 0, -2)
        acquisitionPanel.detailPanel:SetPoint("RIGHT", acquisitionPanel.rightPanel, "RIGHT", -4, 0)
        acquisitionPanel.detailPanel:SetPoint("BOTTOM", acquisitionPanel.rightPanel, "BOTTOM", 0, CONFIG.ACQUISITION_TAB.GALLERY_HEIGHT + 24)
        acquisitionPanel.galleryPanel:Hide()
        acquisitionPanel.infoPanel:Show()
        self:UpdateHeaderPanel(itemID)
        local entry = HDG_AcqData.GetSourceInfo(itemID)
        local si = entry and { type = entry[2], name = entry[3], detail = entry[4] } or nil
        self:UpdateDetailPanel_NonVendor(itemID, si)
        self:UpdateInfoPanel(itemID, si)
    end

    self:UpdateNoteDisplay(itemID)
    if selectedVendor then
        self:UpdateVendorNoteDisplay(selectedVendor.npcID)
        if acquisitionPanel.headerPanel.vendorNoteBtn then acquisitionPanel.headerPanel.vendorNoteBtn:Show() end
        if acquisitionPanel.headerPanel.vendorNoteText then acquisitionPanel.headerPanel.vendorNoteText:Show() end
    else
        if acquisitionPanel.headerPanel.vendorNoteBtn then acquisitionPanel.headerPanel.vendorNoteBtn:Hide() end
        if acquisitionPanel.headerPanel.vendorNoteText then acquisitionPanel.headerPanel.vendorNoteText:Hide() end
    end
    self:UpdatePreviewModel(itemID)
    HDG_AcqRows.UpdateRowHighlights(selectedItemID)
end

-- ============================================================================
-- UPDATE HEADER PANEL
-- ============================================================================

function HDG_AcquisitionTab:UpdateHeaderPanel(itemID)
    if not acquisitionPanel or not itemID then return end
    local hp = acquisitionPanel.headerPanel

    if selectedVendor then
        hp.itemName:SetText(selectedVendor.name or "Unknown Vendor")
        local coordStr = ""
        if HasValidCoords(selectedVendor) then
            coordStr = string.format(" (%.1f, %.1f)", selectedVendor.x, selectedVendor.y)
        end
        hp.subtitle:SetText((selectedVendor.zone or "") .. coordStr)
        hp.subtitle:Show()
        hp.collectedBadge:SetText("")
    else
        hp.itemName:SetText(HDG_Data.GetCachedItemName(itemID) or ("Item #" .. itemID))
        hp.subtitle:SetText("")
        hp.subtitle:Hide()
        local isCollected = HDG_Data.IsItemCollected(itemID)
        if isCollected then
            hp.collectedBadge:SetText(C_Success() .. "Collected|r")
        else
            hp.collectedBadge:SetText(C_Warning() .. "Not Collected|r")
        end
    end
end

-- ============================================================================
-- UPDATE PREVIEW MODEL
-- ============================================================================

function HDG_AcquisitionTab:UpdatePreviewModel(itemID)
    if not acquisitionPanel or not itemID then return end
    local dp = acquisitionPanel.detailPanel
    if not dp.previewScene then return end

    dp.previewScene:Hide()
    dp.previewBg:Show()
    if C_HousingCatalog and C_HousingCatalog.GetCatalogEntryInfoByItem then
        local decorInfo = C_HousingCatalog.GetCatalogEntryInfoByItem(itemID, true)
        if decorInfo and decorInfo.asset then
            local sceneID = decorInfo.uiModelSceneID or CONFIG.ACQUISITION_TAB.DEFAULT_SCENE_ID
            local sOk = pcall(function()
                dp.previewScene:TransitionToModelSceneID(sceneID, CAMERA_TRANSITION_TYPE_IMMEDIATE, CAMERA_MODIFICATION_TYPE_DISCARD, true)
            end)
            if sOk then
                local actor = dp.previewScene:GetActorByTag("decor")
                if actor then
                    actor:SetPreferModelCollisionBounds(true)
                    actor:SetModelByFileID(decorInfo.asset)
                end
                dp.previewScene:Show()
            end
        end
    end
end

-- ============================================================================
-- CATALOG INFO HELPER — shared across all detail panel modes
-- ============================================================================

local SIZE_LABELS = { [0] = "Small", [1] = "Medium", [2] = "Large", [3] = "Extra Large" }

local function ApplyCatalogInfo(dp, itemID, startLine)
    for i = startLine, 9 do
        if dp["line" .. i] then dp["line" .. i]:SetText("") end
    end
    if not C_HousingCatalog or not C_HousingCatalog.GetCatalogEntryInfoByItem then return end
    local info = C_HousingCatalog.GetCatalogEntryInfoByItem(itemID, true)
    if not info then return end

    local line = startLine
    local a = C_Accent()
    local d = C_Dim()

    -- Owned / Placed
    if info.showQuantity then
        local qty = (info.quantity or 0)
        dp["line" .. line]:SetText(a .. "Owned:|r " .. qty .. "  " .. a .. "Placed:|r " .. (info.numPlaced or 0))
        line = line + 1
    end

    -- Placement cost
    if info.placementCost and info.placementCost > 0 and line <= 9 then
        local costStr = HOUSING_DECOR_PLACEMENT_COST_FORMAT and HOUSING_DECOR_PLACEMENT_COST_FORMAT:format(info.placementCost) or tostring(info.placementCost)
        dp["line" .. line]:SetText(a .. "Budget:|r " .. costStr)
        line = line + 1
    end

    -- Size
    if info.size and line <= 9 then
        dp["line" .. line]:SetText(a .. "Size:|r " .. (SIZE_LABELS[info.size] or tostring(info.size)))
        line = line + 1
    end

    -- Placement rules
    local placement = {}
    if info.isAllowedIndoors then table.insert(placement, "Indoor") end
    if info.isAllowedOutdoors then table.insert(placement, "Outdoor") end
    if info.isPrefab then table.insert(placement, "Prefab") end
    if info.isUniqueTrophy then table.insert(placement, "Unique Trophy") end
    if #placement > 0 and line <= 9 then
        dp["line" .. line]:SetText(a .. "Placement:|r " .. table.concat(placement, ", "))
        line = line + 1
    end

    -- Quality
    if info.quality and line <= 9 then
        local r, g, b = GetItemQualityColor(info.quality)
        local hex = string.format("|cff%02x%02x%02x", r * 255, g * 255, b * 255)
        local label = _G["ITEM_QUALITY" .. info.quality .. "_DESC"] or tostring(info.quality)
        dp["line" .. line]:SetText(a .. "Quality:|r " .. hex .. label .. "|r")
        line = line + 1
    end

    -- Source text
    if info.sourceText and info.sourceText ~= "" and line <= 9 then
        dp["line" .. line]:SetText(d .. info.sourceText .. "|r")
        line = line + 1
    end
end

-- ============================================================================
-- UPDATE DETAIL PANEL — Vendor mode
-- ============================================================================

function HDG_AcquisitionTab:UpdateDetailPanel_Vendor(itemID, vendors)
    if not acquisitionPanel or not itemID then return end
    local dp = acquisitionPanel.detailPanel
    local vendor = selectedVendor
    if not vendor then return end

    local itemName = HDG_Data.GetCachedItemName(itemID) or ("Item #" .. itemID)
    local isCollected = HDG_Data.IsItemCollected(itemID)
    local badge = isCollected and (C_Success() .. "Collected|r") or (C_Warning() .. "Not Collected|r")
    dp.line1:SetText(itemName .. "  " .. badge)

    local line = 2
    local repStr = HDG_VendorResolver.FormatRepRequirement(vendor)
    if repStr then
        local met = HDG_VendorResolver.MeetsRepRequirement(vendor)
        local repColor = met and C_Success() or C_Warning()
        dp["line" .. line]:SetText(C_Accent() .. "Reputation:|r " .. repColor .. repStr .. "|r")
        line = line + 1
    end

    local goldCost = vendor.goldCost or 0
    local priceStr = HDG_VendorResolver.FormatGoldCost(goldCost)
    if priceStr then
        dp["line" .. line]:SetText(C_Accent() .. "Cost:|r " .. priceStr)
        line = line + 1
    else
        local currencies = HDG_VendorResolver.GetCurrencyCost(itemID)
        if currencies then
            local iconSize = CONFIG.ACQUISITION_TAB.CURRENCY_ICON_SIZE
            local parts = {}
            for _, c in ipairs(currencies) do
                local iconStr = c.icon and c.icon ~= 0 and string.format("|T%s:%d:%d|t", c.icon, iconSize, iconSize) or ""
                table.insert(parts, string.format("%s %d %s", iconStr, c.amount, c.name or ""))
            end
            dp["line" .. line]:SetText(C_Accent() .. "Cost:|r " .. table.concat(parts, "  "))
            line = line + 1
        else
            dp["line" .. line]:SetText(C_Dim() .. "Visit vendor to see cost|r")
            line = line + 1
        end
    end

    -- Dyeable status
    if HDG_DyeData and HDG_DyeData.IsDecorDyeable(itemID) then
        dp["line" .. line]:SetText(C_Accent() .. "Dyeable:|r " .. C_Success() .. "Yes|r")
        line = line + 1
    end

    ApplyCatalogInfo(dp, itemID, line)

    local hasValidCoords = HasValidCoords(vendor)
    if hasValidCoords then
        acquisitionPanel.waypointBtn:Show()
        if ZONE_MAP_IDS[vendor.zone] then
            acquisitionPanel.mapBtn:Show()
        else
            acquisitionPanel.mapBtn:Hide()
        end
    else
        acquisitionPanel.waypointBtn:Hide()
        acquisitionPanel.mapBtn:Hide()
    end

    dp.otherVendorsLabel:SetText("")
    dp.otherVendorsLabel:Hide()

    -- Vendor note
    self:UpdateVendorNoteDisplay(vendor.npcID)
end

-- ============================================================================
-- UPDATE DETAIL PANEL — Crafted mode
-- ============================================================================

function HDG_AcquisitionTab:UpdateDetailPanel_Crafted(itemID)
    if not acquisitionPanel then return end
    local dp = acquisitionPanel.detailPanel

    acquisitionPanel.waypointBtn:Hide()
    acquisitionPanel.mapBtn:Hide()
    if dp.vendorBtnPool then
        for _, btn in ipairs(dp.vendorBtnPool) do btn:Hide() end
    end
    if dp.moreLabel then dp.moreLabel:Hide() end
    dp.otherVendorsLabel:SetText("")
    dp.otherVendorsLabel:Hide()
    if dp.itemNoteBtn then dp.itemNoteBtn:Hide() end
    if dp.itemNoteText then dp.itemNoteText:Hide() end

    local recipe = HDG_AcqData.recipeByItem[itemID]
    if not recipe then
        dp.line1:SetText(C_Dim() .. "Crafted item (recipe not found)|r")
        for i = 2, 9 do dp["line" .. i]:SetText("") end
        return
    end

    local profIcon = string.format("|T%s:14:14|t", CONFIG.TEXTURES.CRAFTED_ICON)
    dp.line1:SetText(profIcon .. " " .. C_Accent() .. "Profession:|r " .. (recipe.profession or "Unknown"))
    dp.line2:SetText(C_Accent() .. "Expansion:|r " .. (recipe.expansion or "Unknown"))

    -- Reagent list
    local reagentLines = {}
    if recipe.reagents then
        for reagentID, rData in pairs(recipe.reagents) do
            local rName = rData.name or ("Item #" .. reagentID)
            table.insert(reagentLines, string.format("  %dx %s", rData.qty or 1, rName))
        end
        table.sort(reagentLines)
    end
    if #reagentLines > 0 then
        dp.line3:SetText(C_Accent() .. "Reagents:|r")
        dp.line4:SetText(table.concat(reagentLines, "\n"))
    else
        dp.line3:SetText(C_Dim() .. "No reagents listed|r")
        dp.line4:SetText("")
    end

    ApplyCatalogInfo(dp, itemID, 5)
end

-- ============================================================================
-- UPDATE DETAIL PANEL — Non-vendor mode
-- ============================================================================

function HDG_AcquisitionTab:UpdateDetailPanel_NonVendor(itemID, si)
    if not acquisitionPanel then return end
    local dp = acquisitionPanel.detailPanel

    acquisitionPanel.waypointBtn:Hide()
    acquisitionPanel.mapBtn:Hide()
    if dp.vendorBtnPool then
        for _, btn in ipairs(dp.vendorBtnPool) do btn:Hide() end
    end
    if dp.moreLabel then dp.moreLabel:Hide() end
    dp.otherVendorsLabel:SetText("")
    dp.otherVendorsLabel:Hide()
    if dp.itemNoteBtn then dp.itemNoteBtn:Hide() end
    if dp.itemNoteText then dp.itemNoteText:Hide() end

    if not si then
        dp.line1:SetText(C_Dim() .. "Unknown source|r")
        for i = 2, 9 do dp["line" .. i]:SetText("") end
        return
    end

    local typeLabel = HDG_AcqData.GetSourceTypeLabel(si.type, CONFIG)
    local sourceName = si.name or ""

    if si.type == CONFIG.SOURCE_TYPE.PROMOTION then
        local detail = (si.detail and si.detail ~= "" and si.detail ~= "Unknown") and si.detail or ""
        dp.line1:SetText(C_Accent() .. typeLabel .. ":|r " .. detail)
    else
        dp.line1:SetText(C_Accent() .. typeLabel .. ":|r " .. sourceName)
    end

    if si.type ~= CONFIG.SOURCE_TYPE.PROMOTION and si.detail and si.detail ~= "" then
        dp.line2:SetText(C_Dim() .. si.detail .. "|r")
    else
        dp.line2:SetText("")
    end

    local mapID = ZONE_MAP_IDS[si.detail]
    if mapID then
        dp.line3:SetText(C_Dim() .. "Expansion: " .. GetExpansionForMapID(mapID) .. "|r")
    else
        dp.line3:SetText("")
    end

    ApplyCatalogInfo(dp, itemID, 4)
end

-- ============================================================================
-- UPDATE INFO PANEL — Non-vendor bottom section
-- ============================================================================

function HDG_AcquisitionTab:UpdateInfoPanel(itemID, si)
    if not acquisitionPanel then return end
    local ip = acquisitionPanel.infoPanel

    -- Hide crafted action buttons when showing non-crafted items
    if ip.craftQueueBtn then ip.craftQueueBtn:Hide() end
    if ip.viewRecipesBtn then ip.viewRecipesBtn:Hide() end

    if not si then
        ip.sourceLine:SetText(C_Dim() .. "Source Unknown|r")
        ip.expansionLine:SetText("")
        ip.extraLine:SetText("")
        return
    end

    local ST = CONFIG.SOURCE_TYPE
    local typeLabel = HDG_AcqData.GetSourceTypeLabel(si.type, CONFIG)

    if si.type == ST.PROMOTION then
        local detail = (si.detail and si.detail ~= "" and si.detail ~= "Unknown") and si.detail or ""
        ip.sourceLine:SetText(C_Accent() .. typeLabel .. ":|r " .. detail)
    elseif si.type == ST.ACHIEVEMENT and si.detail and si.detail ~= "" then
        ip.sourceLine:SetText(C_Accent() .. typeLabel .. ":|r " .. (si.name or "") .. " (" .. si.detail .. ")")
    else
        ip.sourceLine:SetText(C_Accent() .. typeLabel .. ":|r " .. (si.name or ""))
    end

    local mapID = ZONE_MAP_IDS[si.detail]
    if mapID then
        ip.expansionLine:SetText(C_Dim() .. "Expansion: " .. GetExpansionForMapID(mapID) .. "|r")
    else
        ip.expansionLine:SetText("")
    end

    if si.type == ST.QUEST or si.type == ST.WORLD_QUEST or si.type == ST.DROP then
        if si.detail and si.detail ~= "" then
            ip.extraLine:SetText(C_Dim() .. "Zone: " .. si.detail .. "|r")
        else
            ip.extraLine:SetText("")
        end
    else
        ip.extraLine:SetText("")
    end
end

-- ============================================================================
-- UPDATE INFO PANEL — Crafted bottom section
-- ============================================================================

function HDG_AcquisitionTab:UpdateInfoPanel_Crafted(itemID)
    if not acquisitionPanel then return end
    local ip = acquisitionPanel.infoPanel

    local recipe = HDG_AcqData.recipeByItem[itemID]
    local profName = recipe and recipe.profession or "Unknown"

    ip.sourceLine:SetText(C_Accent() .. "Crafted|r — " .. profName)
    ip.expansionLine:SetText("")
    ip.extraLine:SetText("")

    -- Create or reuse action buttons
    if not ip.craftQueueBtn then
        ip.craftQueueBtn = HDG.UI:Button(ip, "Add to Crafting Queue", 160, 22, function()
            local selRecipe = HDG_AcqData.recipeByItem[selectedItemID]
            if selRecipe then
                local queueItem = {
                    id = selRecipe.itemID,
                    name = selRecipe.name,
                    spell = selRecipe.spellID,
                    reagents = selRecipe.reagents,
                    expansion = HDG_ExpansionData.NormalizeToDisplayName(selRecipe.expansion),
                    profession = selRecipe.profession,
                }
                HDG_UI.AddToShoppingList(queueItem)
                print("|cFF00FF00[HDG]|r Added " .. (selRecipe.name or "item") .. " to crafting queue.")
            end
        end)
        ip.craftQueueBtn:SetPoint("TOPLEFT", ip.sourceLine, "BOTTOMLEFT", 0, -10)
    end

    ip.craftQueueBtn:Show()
    if ip.viewRecipesBtn then ip.viewRecipesBtn:Hide() end
end

-- ============================================================================
-- UPDATE VENDOR INVENTORY (3D Gallery)
-- ============================================================================

function HDG_AcquisitionTab:UpdateVendorInventory(vendor)
    if not acquisitionPanel or not vendor then return end

    HDG_AcqRows.ReleaseAllCells()

    local npcID = vendor.npcID
    local items = HDG_AcqData.vendorItems[npcID]
    if not items or #items == 0 then
        acquisitionPanel.galleryHeader:SetText("No items in inventory")
        acquisitionPanel.costSummary:SetText("")
        return
    end

    local collectedCount = 0
    local uncollectedCost = 0
    local vEntry = HDG_VendorDB and HDG_VendorDB[npcID]
    local vItems = vEntry and vEntry.items
    for _, id in ipairs(items) do
        if HDG_Data.IsItemCollected(id) then
            collectedCount = collectedCount + 1
        else
            local ie = vItems and vItems[id]
            uncollectedCost = uncollectedCost + (ie and ie[1] or 0)
        end
    end

    acquisitionPanel.galleryHeader:SetText(string.format(
        "Also sells: (%d items, %d collected)", #items, collectedCount
    ))

    local uncollectedItemCount = #items - collectedCount
    if uncollectedItemCount > 0 and uncollectedCost > 0 then
        local goldStr = HDG_VendorResolver.FormatGoldCost(uncollectedCost)
        acquisitionPanel.costSummary:SetText(string.format(
            "%d uncollected — %s total", uncollectedItemCount, goldStr or "?"
        ))
    else
        acquisitionPanel.costSummary:SetText("")
    end

    -- Build grid
    local content = acquisitionPanel.galleryContent
    local ACQ = CONFIG.ACQUISITION_TAB
    local cellSize = ACQ.CELL_SIZE
    local spacing = ACQ.CELL_SPACING
    local contentWidth = content:GetWidth()
    if contentWidth == 0 then contentWidth = 400 end

    local cellsPerRow = math.max(1, math.floor(contentWidth / (cellSize + spacing)))
    local cellHeight = cellSize + 14 + spacing

    local xPos = 0
    local yPos = 0
    local col = 0

    local sortedItems = {}
    for _, id in ipairs(items) do table.insert(sortedItems, id) end
    table.sort(sortedItems, function(a, b)
        local aCollected = HDG_Data.IsItemCollected(a)
        local bCollected = HDG_Data.IsItemCollected(b)
        if aCollected ~= bCollected then return not aCollected end
        return (HDG_Data.GetCachedItemName(a) or "") < (HDG_Data.GetCachedItemName(b) or "")
    end)

    local onSelect = function(id) HDG_AcquisitionTab:SelectItem(id) end
    for _, id in ipairs(sortedItems) do
        xPos = col * (cellSize + spacing)
        HDG_AcqRows.CreateGalleryCell(content, id, xPos, yPos, cellSize, selectedItemID, onSelect, CONFIG)

        col = col + 1
        if col >= cellsPerRow then
            col = 0
            yPos = yPos - cellHeight
        end
    end

    local totalRows = math.ceil(#sortedItems / cellsPerRow)
    content:SetHeight(totalRows * cellHeight + 10)
end

-- ============================================================================
-- LIFECYCLE
-- ============================================================================

function HDG_AcquisitionTab:Init(configRef, mainFrameRef, _dbRef)
    CONFIG = configRef
    mainFrame = mainFrameRef
end

function HDG_AcquisitionTab:Show()
    if not acquisitionPanel then
        HDG_AcqData.BuildCaches(CONFIG)
        self:CreatePanel()
    end
    acquisitionPanel:Show()
    RefreshGuideText()
    C_Timer.After(0, function()
        if acquisitionPanel and acquisitionPanel:IsShown() then
            HDG_AcquisitionTab:RefreshList()
        end
    end)
end

function HDG_AcquisitionTab:Hide()
    if acquisitionPanel then
        acquisitionPanel:Hide()
    end
end

function HDG_AcquisitionTab:IsShown()
    return acquisitionPanel and acquisitionPanel:IsShown()
end

function HDG_AcquisitionTab:Refresh()
    if not CONFIG then return end
    if not acquisitionPanel then
        HDG_AcqData.BuildCaches(CONFIG)
        self:CreatePanel()
    end
    self:RefreshList()
    if selectedItemID then
        self:SelectItem(selectedItemID)
    end
end

-- ============================================================================
-- STATIC POPUP — User Note Editor
-- ============================================================================

StaticPopupDialogs["HDG_EDIT_USER_NOTE"] = {
    text = "Note for %s:",
    button1 = "Save",
    button2 = "Cancel",
    hasEditBox = true,
    editBoxWidth = 280,
    OnShow = function(self)
        local note = HDG.Store:GetUserNote(self.data)
        self.EditBox:SetText(note and note.text or "")
        self.EditBox:SetMaxLetters(CONFIG and CONFIG.ACQUISITION_TAB.MAX_NOTE_LENGTH or 200)
        self.EditBox:HighlightText()
    end,
    OnAccept = function(self)
        local text = strtrim(self.EditBox:GetText())
        if text == "" then
            HDG.Store:RemoveUserNote(self.data)
        else
            HDG.Store:SetUserNote(self.data, text)
        end
        HDG_AcquisitionTab:UpdateNoteDisplay(self.data)
        HDG_AcquisitionTab:RefreshList()
    end,
    EditBoxOnEnterPressed = function(self)
        local parent = self:GetParent()
        local text = strtrim(self:GetText())
        if text == "" then
            HDG.Store:RemoveUserNote(parent.data)
        else
            HDG.Store:SetUserNote(parent.data, text)
        end
        HDG_AcquisitionTab:UpdateNoteDisplay(parent.data)
        HDG_AcquisitionTab:RefreshList()
        parent:Hide()
    end,
    timeout = 0,
    whileDead = true,
    hideOnEscape = true,
    preferredIndex = 3,
}

-- ============================================================================
-- STATIC POPUP — Vendor Note Editor
-- ============================================================================

StaticPopupDialogs["HDG_EDIT_VENDOR_NOTE"] = {
    text = "Vendor note for %s:",
    button1 = "Save",
    button2 = "Cancel",
    hasEditBox = true,
    editBoxWidth = 280,
    OnShow = function(self)
        local note = HDG.Store:GetVendorNote(self.data)
        self.EditBox:SetText(note and note.text or "")
        self.EditBox:SetMaxLetters(CONFIG and CONFIG.ACQUISITION_TAB.MAX_NOTE_LENGTH or 200)
        self.EditBox:HighlightText()
    end,
    OnAccept = function(self)
        local text = strtrim(self.EditBox:GetText())
        if text == "" then
            HDG.Store:RemoveVendorNote(self.data)
        else
            HDG.Store:SetVendorNote(self.data, text)
        end
        HDG_AcquisitionTab:UpdateVendorNoteDisplay(self.data)
        HDG_AcquisitionTab:RefreshList()
    end,
    EditBoxOnEnterPressed = function(self)
        local parent = self:GetParent()
        local text = strtrim(self:GetText())
        if text == "" then
            HDG.Store:RemoveVendorNote(parent.data)
        else
            HDG.Store:SetVendorNote(parent.data, text)
        end
        HDG_AcquisitionTab:UpdateVendorNoteDisplay(parent.data)
        HDG_AcquisitionTab:RefreshList()
        parent:Hide()
    end,
    timeout = 0,
    whileDead = true,
    hideOnEscape = true,
    preferredIndex = 3,
}

-- ============================================================================
-- NOTE DISPLAY HELPERS
-- ============================================================================

function HDG_AcquisitionTab:UpdateNoteDisplay(itemID)
    if not acquisitionPanel or not acquisitionPanel.detailPanel then return end
    local dp = acquisitionPanel.detailPanel
    if not dp.itemNoteText then return end

    local note = HDG.Store:GetUserNote(itemID)
    if note and note.text ~= "" then
        dp.itemNoteText:SetText(C_Accent() .. "Note:|r " .. C_Text() .. note.text .. "|r")
    else
        dp.itemNoteText:SetText(C_Dim() .. "Add item note...|r")
    end
    dp.itemNoteBtn:Show()
    dp.itemNoteText:Show()
end

function HDG_AcquisitionTab:UpdateVendorNoteDisplay(npcID)
    if not acquisitionPanel or not acquisitionPanel.headerPanel then return end
    local hp = acquisitionPanel.headerPanel
    if not hp.vendorNoteText then return end

    local note = npcID and HDG.Store:GetVendorNote(npcID)
    if note and note.text ~= "" then
        hp.vendorNoteText:SetText(C_Text() .. note.text .. "|r")
    else
        hp.vendorNoteText:SetText(C_Dim() .. "Add vendor note...|r")
    end
end

-- ============================================================================
-- EXPORT NOTES WINDOW
-- ============================================================================

ShowNotesExportWindow = function()
    local exportText = HDG.Store:ExportUserNotes()

    if not HDG_NotesExportFrame then
        local f = CreateFrame("Frame", "HDG_NotesExportFrame", UIParent, "BackdropTemplate")
        f:SetSize(500, 350)
        f:SetPoint("CENTER")
        f:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8X8",
            edgeFile = "Interface\\Buttons\\WHITE8X8",
            tile = false, edgeSize = 2,
            insets = { left = 0, right = 0, top = 0, bottom = 0 }
        })
        local fScheme = GetScheme()
        if fScheme then
            f:SetBackdropColor(fScheme.panel.r, fScheme.panel.g, fScheme.panel.b, 0.95)
            f:SetBackdropBorderColor(fScheme.accent.r, fScheme.accent.g, fScheme.accent.b, 1)
            RegisterWidget(f, "Panel")
        else
            f:SetBackdropColor(0.1, 0.1, 0.12, 0.95)
            f:SetBackdropBorderColor(0.08, 0.72, 0.65, 1)
        end
        f:SetFrameStrata("DIALOG")
        f:SetMovable(true)
        f:EnableMouse(true)
        f:RegisterForDrag("LeftButton")
        f:SetScript("OnDragStart", f.StartMoving)
        f:SetScript("OnDragStop", f.StopMovingOrSizing)

        local exportTitle = f:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        exportTitle:SetPoint("TOP", 0, -10)
        if fScheme then ApplyFont(exportTitle, fScheme, "header") end
        exportTitle:SetText(C_Accent() .. "Export User Notes|r")

        local instr = f:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        instr:SetPoint("TOP", exportTitle, "BOTTOM", 0, -5)
        instr:SetText(C_Dim() .. "Select all (Ctrl+A) then copy (Ctrl+C)|r")
        if fScheme then ApplyFont(instr, fScheme, "body") end

        local sf = CreateFrame("ScrollFrame", nil, f, "UIPanelScrollFrameTemplate")
        sf:SetPoint("TOPLEFT", 15, -50)
        sf:SetPoint("BOTTOMRIGHT", -35, 45)

        local eb = CreateFrame("EditBox", nil, sf)
        eb:SetMultiLine(true)
        if fScheme and fScheme.fonts and fScheme.fonts.small then
            eb:SetFont(fScheme.fonts.small.file or "Fonts\\FRIZQT__.TTF", fScheme.fonts.small.size or 10, "")
        else
            eb:SetFontObject("GameFontHighlightSmall")
        end
        eb:SetWidth(440)
        eb:SetAutoFocus(false)
        eb:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
        sf:SetScrollChild(eb)
        f.editBox = eb

        local closeBtn = CreateFrame("Button", nil, f, "UIPanelButtonTemplate")
        closeBtn:SetPoint("BOTTOM", 0, 10)
        closeBtn:SetSize(80, 22)
        closeBtn:SetText("Close")
        closeBtn:SetScript("OnClick", function() f:Hide() end)

        HDG_NotesExportFrame = f
    end

    HDG_NotesExportFrame.editBox:SetText(exportText)
    HDG_NotesExportFrame.editBox:HighlightText()
    HDG_NotesExportFrame.editBox:SetFocus()
    HDG_NotesExportFrame:Show()
end

-- Expose to privateDB for other modules
addon.HDG_AcquisitionTab = HDG_AcquisitionTab
