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

-- ============================================================================
-- VENDOR SHOPPING LIST + ZONE SCANNER
-- Floating widget with List / Zone / Config modes
-- ============================================================================

HDG_VendorShoppingList = {}

local widget          -- FloatingWidget result table
local CONFIG
local scrollFrame, scrollContent
local configContent   -- static frame for config mode (no scroll)
local created = false
local activeMode = "list"  -- "list", "zone", "config"
local currentZoneMapID = nil
local currentZoneVendors = nil
local rowPool = {}
local modeButtons = {}
local expandedVendors = {} -- [npcID] = true for collapsed/expanded state

local GetScheme = function() return HDG_UIHelpers and HDG_UIHelpers.GetScheme() end
local RegisterWidget = function(w, t) if HDG_UIHelpers then HDG_UIHelpers.RegisterWidget(w, t) end end
local ApplyFont = function(fs, s, sz) if HDG.Theme then HDG.Theme.ApplyFont(fs, s, sz) end end

local function C_Accent()  return CONFIG and CONFIG.TEXT_COLORS.ACCENT or "|cFF14b8a6" end
local function C_Dim()     return CONFIG and CONFIG.TEXT_COLORS.MUTED or "|cFF888888" end
local function C_Success() return CONFIG and CONFIG.TEXT_COLORS.SUCCESS or "|cFF00FF00" end
local function C_Gold()    return CONFIG and CONFIG.TEXT_COLORS.GOLD or "|cFFfcd34d" end

local function GetConfig(key)
    return HDG.Store and HDG.Store:GetState().config[key]
end

local function Dispatch(action, payload)
    if HDG.Store then HDG.Store:Dispatch(action, payload) end
end

-- ============================================================================
-- ROW POOLING
-- ============================================================================

local function GetPooledRow(parent, rowHeight)
    for _, row in ipairs(rowPool) do
        if not row.inUse then
            row.inUse = true
            row:SetParent(parent)
            row:SetHeight(rowHeight)
            row:Show()
            -- Reset
            if row.iconTex then row.iconTex:Hide() end
            if row.removeBtn then row.removeBtn:Hide() end
            if row.nameText then row.nameText:SetText(""); row.nameText:SetWordWrap(false) end
            if row.rightText then row.rightText:SetText("") end
            if row.badgeText then row.badgeText:SetText(""); row.badgeText:Hide() end
            if row.expandArrow then row.expandArrow:Hide() end
            row:SetScript("OnClick", nil)
            row:SetScript("OnEnter", nil)
            row:SetScript("OnLeave", nil)
            return row
        end
    end

    local row = CreateFrame("Button", nil, parent, "BackdropTemplate")
    row:SetHeight(rowHeight)
    row.inUse = true

    row:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 1 })

    -- Icon (hidden by default)
    local icon = row:CreateTexture(nil, "ARTWORK")
    icon:SetSize(16, 16)
    icon:SetPoint("LEFT", 4, 0)
    icon:Hide()
    row.iconTex = icon

    -- Expand/collapse arrow
    local arrow = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    arrow:SetPoint("LEFT", 2, 0)
    arrow:Hide()
    row.expandArrow = arrow

    -- Name
    local nameText = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    nameText:SetPoint("LEFT", 4, 0)
    nameText:SetJustifyH("LEFT")
    nameText:SetWordWrap(false)
    row.nameText = nameText

    -- Right-aligned text (gold cost, counts, etc.)
    local rightText = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    rightText:SetPoint("RIGHT", -4, 0)
    rightText:SetJustifyH("RIGHT")
    rightText:SetWordWrap(false)
    row.rightText = rightText

    -- Badge (count, rep, etc.)
    local badge = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightExtraSmall")
    badge:SetJustifyH("LEFT")
    badge:Hide()
    row.badgeText = badge

    -- Remove button (X) — hidden by default
    local removeBtn = CreateFrame("Button", nil, row)
    removeBtn:SetSize(14, 14)
    removeBtn:SetPoint("RIGHT", -2, 0)
    local removeTex = removeBtn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    removeTex:SetPoint("CENTER")
    removeTex:SetText("|cFFFF4444x|r")
    removeBtn:Hide()
    row.removeBtn = removeBtn

    table.insert(rowPool, row)
    return row
end

local function ReleaseAllRows()
    for _, row in ipairs(rowPool) do
        row.inUse = false
        row:Hide()
        row:ClearAllPoints()
    end
end

-- ============================================================================
-- HELPERS
-- ============================================================================

local function FormatGoldCompact(copper)
    if not copper or copper == 0 then return "" end
    local gold = math.floor(copper / 10000)
    if gold > 0 then return C_Gold() .. gold .. "g|r" end
    local silver = math.floor((copper % 10000) / 100)
    return C_Dim() .. silver .. "s|r"
end

local function SetWaypoint(mapID, x, y, title)
    if InCombatLockdown() then
        print("|cFFFF0000[HDG]|r Cannot set waypoints during combat.")
        return false
    end
    if not TomTom or not TomTom.AddWaypoint then
        print("|cFFFFAA00[HDG]|r Install TomTom for waypoint support.")
        return false
    end
    local uiMapID, mapX, mapY = HDG_VendorResolver.ZonePctToMap(mapID, x, y)
    if not uiMapID then return false end
    local ok = pcall(function()
        TomTom:AddWaypoint(uiMapID, mapX, mapY, {
            title = title or "Vendor",
            persistent = false,
            from = "HDG",
        })
    end)
    return ok
end

-- ============================================================================
-- CREATE UI (lazy, on first show)
-- ============================================================================

local function CreatePanel()
    if created then return end
    created = true

    local FW = CONFIG.FLOATING_WIDGET
    local panelWidth = FW.VENDOR_LIST_WIDTH
    local panelHeight = FW.VENDOR_LIST_HEIGHT

    widget = HDG.UI:FloatingWidget({
        name = "HDG_VendorShoppingListFrame",
        width = panelWidth,
        height = panelHeight,
        strata = "DIALOG",
        onPositionSaved = function(point, relativePoint, x, y)
            if HDG.Store then
                HDG.Store.state.ui.vendorListPosition = { point, relativePoint, x, y }
                HDG.Store:QueueSave()
            end
        end,
    })

    local frame = widget.frame

    -- Restore saved position
    if HDG.Store then
        local pos = HDG.Store:GetState().ui.vendorListPosition
        if pos and pos[1] then
            frame:ClearAllPoints()
            frame:SetPoint(pos[1], UIParent, pos[2] or pos[1], pos[3] or 0, pos[4] or 0)
        end
    end

    frame:Hide()

    -- Hide flat backdrop border (keep dark fill for content area)
    frame:SetBackdropBorderColor(0, 0, 0, 0)

    -- Midnight parchment background (subtle texture behind dark fill)
    local parchment = frame:CreateTexture(nil, "BACKGROUND", nil, 1)
    parchment:SetAtlas("ui-frame-midnight-cardparchmentwider")
    parchment:SetAllPoints()
    parchment:SetAlpha(0.15)

    -- Ornate midnight border on overlay frame (renders above scroll content)
    local borderOverlay = CreateFrame("Frame", nil, frame)
    borderOverlay:SetAllPoints()
    borderOverlay:SetFrameLevel(frame:GetFrameLevel() + 20)
    local borderTex = borderOverlay:CreateTexture(nil, "OVERLAY", nil, 7)
    borderTex:SetAtlas("ui-frame-midnight-border")
    borderTex:SetPoint("TOPLEFT", -12, 12)
    borderTex:SetPoint("BOTTOMRIGHT", 12, -12)
    borderTex:SetTextureSliceMargins(45, 45, 45, 45)
    borderTex:SetTextureSliceMode(Enum.UITextureSliceMode.Tiled)

    -- Midnight title bar: left cap + tiling middle + right cap
    local titleH = FW.HEADER_HEIGHT
    local capW = math.floor(202 * (titleH / 85))  -- maintain aspect ratio (~81px at 34px height)

    local titleLeft = frame:CreateTexture(nil, "ARTWORK", nil, 1)
    titleLeft:SetAtlas("ui-frame-midnight-titleleft")
    titleLeft:SetPoint("TOPLEFT", 0, 0)
    titleLeft:SetSize(capW, titleH)

    local titleRight = frame:CreateTexture(nil, "ARTWORK", nil, 1)
    titleRight:SetAtlas("ui-frame-midnight-titleright")
    titleRight:SetPoint("TOPRIGHT", 0, 0)
    titleRight:SetSize(capW, titleH)

    local titleMid = frame:CreateTexture(nil, "ARTWORK", nil, 0)
    titleMid:SetAtlas("_ui-frame-midnight-titlemiddle")
    titleMid:SetPoint("TOPLEFT", titleLeft, "TOPRIGHT")
    titleMid:SetPoint("BOTTOMRIGHT", titleRight, "BOTTOMLEFT")
    titleMid:SetHorizTile(true)

    -- ====================================================================
    -- HEADER: Title + Close + Map Waypoints + Clear All
    -- ====================================================================
    local scheme = GetScheme()

    -- Title with drop shadow
    local titleShadow = frame:CreateFontString(nil, "ARTWORK", nil, 0)
    titleShadow:SetFont(STANDARD_TEXT_FONT, 13, "")
    titleShadow:SetPoint("TOP", 1, -14)
    titleShadow:SetTextColor(0, 0, 0, 0.8)
    titleShadow:SetText("Shopping List")

    local titleText = frame:CreateFontString(nil, "ARTWORK", nil, 1)
    titleText:SetFont(STANDARD_TEXT_FONT, 13, "")
    titleText:SetPoint("TOP", 0, -13)
    titleText:SetTextColor(0.85, 0.75, 0.45, 1) -- warm gold to match Midnight frame
    titleText:SetShadowOffset(1, -1)
    titleText:SetShadowColor(0, 0, 0, 0.6)
    titleText:SetText("Shopping List")

    -- Header action buttons (shifted left 10px, reduced 3px)
    local closeBtnSize = FW.HEADER_BTN_SIZE - 5
    local hdrX = -(FW.HEADER_BTN_MARGIN + 10)
    local hdrY = -(FW.HEADER_BTN_MARGIN - 2)

    -- Close button (above border overlay)
    local closeBtn = widget.CreateHeaderButton(closeBtnSize, "Close")
    closeBtn:ClearAllPoints()
    closeBtn:SetPoint("TOPRIGHT", hdrX, hdrY)
    closeBtn:SetFrameLevel(frame:GetFrameLevel() + 25)
    local closeTex = closeBtn:CreateTexture(nil, "ARTWORK")
    closeTex:SetSize(10, 10)
    closeTex:SetPoint("CENTER")
    closeTex:SetTexture(CONFIG.ADDON_TEXTURES.CLOSE)
    closeBtn:SetScript("OnClick", function() frame:Hide() end)

    -- Map Waypoints button — positioned after mode tabs below

    -- Clear All button (List mode only) — positioned left of mode tabs
    local clearBtn = widget.CreateHeaderButton(closeBtnSize, "Clear All")
    local clearIcon = clearBtn:CreateTexture(nil, "ARTWORK")
    clearIcon:SetSize(12, 12)
    clearIcon:SetPoint("CENTER")
    clearIcon:SetAtlas("transmog-icon-remove")
    clearBtn:SetScript("OnClick", function()
        if HDG.Store then
            HDG.Store:ClearVendorShoppingList()
            HDG_VendorShoppingList.Refresh()
        end
    end)
    widget.clearBtn = clearBtn

    -- ESC key closes the shopping list
    table.insert(UISpecialFrames, "HDG_VendorShoppingListFrame")

    -- ====================================================================
    -- MODE TAB BAR (List / Zone / Config)
    -- ====================================================================
    local modeBarY = -(FW.HEADER_HEIGHT)
    local modeBar = CreateFrame("Frame", nil, frame)
    modeBar:SetPoint("TOPLEFT", 4, modeBarY)
    modeBar:SetPoint("TOPRIGHT", -4, modeBarY)
    modeBar:SetHeight(FW.VENDOR_LIST_MODE_BAR_HEIGHT)

    local modes = { { key = "list", label = "List" }, { key = "zone", label = "Zone" }, { key = "config", label = "Config" } }
    local btnW = FW.VENDOR_LIST_MODE_BTN_WIDTH
    local btnGap = 2
    local totalTabW = #modes * btnW + (#modes - 1) * btnGap
    local tabStartX = (FW.VENDOR_LIST_WIDTH - 8 - totalTabW) / 2 -- 8 = modeBar L+R padding
    for i, m in ipairs(modes) do
        local btn = HDG.UI:Button(modeBar, m.label, btnW, FW.VENDOR_LIST_MODE_BAR_HEIGHT - 2, function()
            activeMode = m.key
            HDG_VendorShoppingList.Refresh()
        end)
        btn:SetPoint("TOPLEFT", tabStartX + (i - 1) * (btnW + btnGap), -1)
        modeButtons[m.key] = btn
    end

    -- Position clear button to left of List tab
    clearBtn:ClearAllPoints()
    clearBtn:SetPoint("RIGHT", modeButtons["list"], "LEFT", -btnGap, 0)

    -- ====================================================================
    -- MAP WAYPOINTS ROW (below mode tabs)
    -- ====================================================================
    local mapRowY = modeBarY - FW.VENDOR_LIST_MODE_BAR_HEIGHT - 2
    local mapRowH = 22
    local mapBtn = HDG.UI:Button(frame, "Map Waypoints", FW.VENDOR_LIST_WIDTH - 16, mapRowH, function()
        if activeMode == "list" then
            HDG_VendorShoppingList.MapListWaypoints()
        elseif activeMode == "zone" then
            HDG_VendorShoppingList.MapZoneWaypoints()
        end
    end)
    mapBtn:SetPoint("TOP", 0, mapRowY)
    local mapIcon = mapBtn:CreateTexture(nil, "ARTWORK")
    mapIcon:SetSize(12, 12)
    mapIcon:SetPoint("LEFT", 8, 0)
    mapIcon:SetAtlas("Waypoint-MapPin-ChatIcon")
    widget.mapBtn = mapBtn

    -- ====================================================================
    -- SCROLL PANEL (List + Zone modes)
    -- ====================================================================
    local scrollTop = mapRowY - mapRowH - 2

    scrollFrame = CreateFrame("ScrollFrame", nil, frame, "UIPanelScrollFrameTemplate")
    scrollFrame:SetPoint("TOPLEFT", 4, scrollTop)
    scrollFrame:SetPoint("BOTTOMRIGHT", -20, 4)

    scrollContent = CreateFrame("Frame", nil, scrollFrame)
    scrollContent:SetWidth(panelWidth - 28)
    scrollContent:SetHeight(1)
    scrollFrame:SetScrollChild(scrollContent)

    -- Style scrollbar with minimal-scrollbar atlas
    local scrollBar = scrollFrame.ScrollBar
    if scrollBar then
        scrollBar:SetWidth(8)

        -- Hide default arrow buttons
        if scrollBar.ScrollUpButton then scrollBar.ScrollUpButton:SetAlpha(0); scrollBar.ScrollUpButton:SetSize(1, 1) end
        if scrollBar.ScrollDownButton then scrollBar.ScrollDownButton:SetAlpha(0); scrollBar.ScrollDownButton:SetSize(1, 1) end

        -- Track background
        local trackBg = scrollBar:CreateTexture(nil, "BACKGROUND")
        trackBg:SetAtlas("!minimal-scrollbar-track-middle")
        trackBg:SetPoint("TOPLEFT", 0, 0)
        trackBg:SetPoint("BOTTOMRIGHT", 0, 0)
        trackBg:SetAlpha(0.3)

        -- Thumb
        local thumb = scrollBar:GetThumbTexture()
        if thumb then
            thumb:SetAtlas("minimal-scrollbar-thumb-middle")
            thumb:SetWidth(8)
            thumb:SetAlpha(0.7)
        end
    end

    -- ====================================================================
    -- CONFIG CONTENT (static, no scroll)
    -- ====================================================================
    configContent = CreateFrame("Frame", nil, frame)
    configContent:SetPoint("TOPLEFT", 4, scrollTop)
    configContent:SetPoint("BOTTOMRIGHT", -4, 4)
    configContent:Hide()

    local configWidth = panelWidth - 16 -- account for padding
    local checkY = -10
    local function MakeCheckbox(label, hint, configKey)
        local cb = CreateFrame("CheckButton", nil, configContent, "UICheckButtonTemplate")
        cb:SetSize(26, 26)
        cb:SetPoint("TOPLEFT", 8, checkY)
        cb:SetChecked(GetConfig(configKey) or false)

        local cbLabel = configContent:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        cbLabel:SetPoint("LEFT", cb, "RIGHT", 5, 0)
        cbLabel:SetPoint("RIGHT", configContent, "RIGHT", -8, 0)
        if scheme then ApplyFont(cbLabel, scheme, "body") end
        cbLabel:SetText("|cFFe2e8f0" .. label .. "|r")
        cbLabel:SetWordWrap(false)
        cbLabel:SetJustifyH("LEFT")

        if hint then
            local hintLabel = configContent:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            hintLabel:SetPoint("TOPLEFT", cb, "BOTTOMLEFT", 26, -2)
            hintLabel:SetPoint("RIGHT", configContent, "RIGHT", -8, 0)
            if scheme then ApplyFont(hintLabel, scheme, "small") end
            hintLabel:SetText(C_Dim() .. hint .. "|r")
            hintLabel:SetWordWrap(true)
            hintLabel:SetJustifyH("LEFT")
        end

        cb:SetScript("OnClick", function(self)
            Dispatch("SET_CONFIG", { key = configKey, value = self:GetChecked() })
        end)

        checkY = checkY - FW.VENDOR_LIST_CONFIG_CHECKBOX_GAP
        return cb
    end

    widget.cbZoneScanner = MakeCheckbox(
        "Zone Scanner",
        "Auto-open when entering a zone with uncollected vendor items",
        "zoneScannerEnabled"
    )
    widget.cbZoneSound = MakeCheckbox(
        "Play Sound on Auto-Open",
        "Notification sound when zone scanner triggers",
        "zoneScannerSound"
    )
end

-- ============================================================================
-- UPDATE MODE TAB HIGHLIGHTS
-- ============================================================================

local function UpdateModeHighlights()
    local s = GetScheme()
    if not s then return end
    for key, btn in pairs(modeButtons) do
        local isActive = (key == activeMode)
        -- Swap normal texture atlas for active/inactive state
        local normalTex = btn:GetNormalTexture()
        if normalTex then
            normalTex:SetAtlas(isActive and "UI-DialogBox-Button-Down" or "UI-DialogBox-Button-Up")
        end
        local fs = btn:GetFontString()
        if fs then
            if isActive then
                fs:SetTextColor(s.text.r, s.text.g, s.text.b, s.text.a)
            else
                fs:SetTextColor(s.text_dim.r, s.text_dim.g, s.text_dim.b, s.text_dim.a)
            end
        end
    end
end

-- ============================================================================
-- LIST MODE
-- ============================================================================

local function RefreshListMode()
    if not HDG.Store then return end

    local list = HDG.Store:GetState().vendorShoppingList
    local FW = CONFIG.FLOATING_WIDGET

    ReleaseAllRows()

    if #list == 0 then
        local row = GetPooledRow(scrollContent, 40)
        row:SetPoint("TOPLEFT", 0, 0)
        row:SetPoint("RIGHT", scrollContent, "RIGHT", 0, 0)
        row:SetBackdropColor(0, 0, 0, 0)
        row:SetBackdropBorderColor(0, 0, 0, 0)
        row.nameText:ClearAllPoints()
        row.nameText:SetPoint("LEFT", 8, 0)
        row.nameText:SetWidth(scrollContent:GetWidth() - 16)
        row.nameText:SetText(C_Dim() .. "No items on your list.\nShift-Click items in the Acquisition tab to add them.|r")
        row.nameText:SetWordWrap(true)
        scrollContent:SetHeight(40)
        return
    end

    -- Build zone → vendor → items hierarchy
    local zones = {}
    local zoneOrder = {}
    for _, entry in ipairs(list) do
        local npcID = entry.npcID
        local ve = HDG_VendorDB and HDG_VendorDB[npcID]
        local zoneName = ve and ve[2] or "Unknown"
        if not zones[zoneName] then
            zones[zoneName] = { vendors = {} }
            table.insert(zoneOrder, zoneName)
        end
        if not zones[zoneName].vendors[npcID] then
            zones[zoneName].vendors[npcID] = { items = {}, totalGold = 0 }
        end
        local goldCost = ve and ve.items and ve.items[entry.itemID] and ve.items[entry.itemID][1] or 0
        table.insert(zones[zoneName].vendors[npcID].items, entry)
        zones[zoneName].vendors[npcID].totalGold = zones[zoneName].vendors[npcID].totalGold + goldCost
    end
    table.sort(zoneOrder)

    local yOffset = 0
    local contentWidth = scrollContent:GetWidth()
    local scheme = GetScheme()

    for _, zoneName in ipairs(zoneOrder) do
        local zoneData = zones[zoneName]

        -- Zone total gold
        local zoneTotalGold = 0
        for _, vd in pairs(zoneData.vendors) do zoneTotalGold = zoneTotalGold + vd.totalGold end

        -- Zone header row
        local zRow = GetPooledRow(scrollContent, FW.VENDOR_LIST_ZONE_HEADER_HEIGHT)
        zRow:SetPoint("TOPLEFT", 0, -yOffset)
        zRow:SetPoint("RIGHT", scrollContent, "RIGHT", 0, 0)
        if scheme then
            zRow:SetBackdropColor(scheme.accent.r, scheme.accent.g, scheme.accent.b, 0.15)
            zRow:SetBackdropBorderColor(scheme.accent.r, scheme.accent.g, scheme.accent.b, 0.3)
        end
        zRow.nameText:ClearAllPoints()
        zRow.nameText:SetPoint("LEFT", 6, 0)
        zRow.nameText:SetWidth(contentWidth - 60)
        zRow.nameText:SetText("|cFFe2e8f0" .. zoneName .. "|r")
        if scheme then ApplyFont(zRow.nameText, scheme, "body") end
        zRow.rightText:SetText(FormatGoldCompact(zoneTotalGold))
        if scheme then ApplyFont(zRow.rightText, scheme, "small") end
        yOffset = yOffset + FW.VENDOR_LIST_ZONE_HEADER_HEIGHT

        -- Sort vendors by name
        local vendorOrder = {}
        for npcID, _ in pairs(zoneData.vendors) do table.insert(vendorOrder, npcID) end
        table.sort(vendorOrder, function(a, b)
            return (HDG_VendorResolver.GetVendorNameByID(a) or "") < (HDG_VendorResolver.GetVendorNameByID(b) or "")
        end)

        for _, npcID in ipairs(vendorOrder) do
            local vData = zoneData.vendors[npcID]
            local vendorName = HDG_VendorResolver.GetVendorNameByID(npcID)
            local ve = HDG_VendorDB and HDG_VendorDB[npcID]
            local isExpanded = expandedVendors[npcID]

            -- Vendor row
            local vRow = GetPooledRow(scrollContent, FW.VENDOR_LIST_VENDOR_ROW_HEIGHT)
            vRow:SetPoint("TOPLEFT", FW.VENDOR_LIST_INDENT_VENDOR, -yOffset)
            vRow:SetPoint("RIGHT", scrollContent, "RIGHT", 0, 0)
            if scheme then
                vRow:SetBackdropColor(scheme.bg.r, scheme.bg.g, scheme.bg.b, 0.3)
                vRow:SetBackdropBorderColor(0, 0, 0, 0)
            end

            -- Expand arrow
            vRow.expandArrow:ClearAllPoints()
            vRow.expandArrow:SetPoint("LEFT", 2, 0)
            vRow.expandArrow:SetText(isExpanded and C_Dim() .. "v|r" or C_Dim() .. ">|r")
            vRow.expandArrow:Show()

            vRow.nameText:ClearAllPoints()
            vRow.nameText:SetPoint("LEFT", 14, 0)
            vRow.nameText:SetWidth(contentWidth - FW.VENDOR_LIST_INDENT_VENDOR - 60)
            vRow.nameText:SetText(C_Accent() .. vendorName .. "|r")
            if scheme then ApplyFont(vRow.nameText, scheme, "body") end

            -- Item count badge
            vRow.badgeText:ClearAllPoints()
            vRow.badgeText:SetPoint("LEFT", vRow.nameText, "RIGHT", 4, 0)
            vRow.badgeText:SetText(C_Dim() .. "(" .. #vData.items .. ")|r")
            vRow.badgeText:Show()

            -- Click → toggle expand/collapse, shift-click → waypoint
            local vNpcID = npcID
            vRow:SetScript("OnClick", function()
                if IsShiftKeyDown() then
                    if ve then
                        if SetWaypoint(ve[3], ve[4], ve[5], vendorName) then
                            print(C_Success() .. "[HDG]|r Waypoint set for " .. C_Accent() .. vendorName .. "|r")
                        end
                    end
                else
                    expandedVendors[vNpcID] = not expandedVendors[vNpcID]
                    HDG_VendorShoppingList.Refresh()
                end
            end)
            vRow:SetScript("OnEnter", function(self)
                local c = GetScheme()
                if c then self:SetBackdropColor(c.button_hover.r, c.button_hover.g, c.button_hover.b, 0.3) end
                GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
                GameTooltip:SetText(vendorName, 1, 1, 1)
                if ve then
                    GameTooltip:AddLine(ve[2] or "Unknown Zone", 0.7, 0.7, 0.7)
                    if ve[4] and ve[5] and ve[4] > 0 then
                        GameTooltip:AddLine(string.format("%.1f, %.1f", ve[4], ve[5]), 0.5, 0.5, 0.5)
                    end
                end
                GameTooltip:AddLine(" ")
                GameTooltip:AddLine("Click to expand/collapse", 0.5, 0.5, 0.5)
                GameTooltip:AddLine("Shift-Click to set TomTom waypoint", 0.5, 0.5, 0.5)
                GameTooltip:Show()
            end)
            vRow:SetScript("OnLeave", function(self)
                local c = GetScheme()
                if c then self:SetBackdropColor(c.bg.r, c.bg.g, c.bg.b, 0.3) end
                GameTooltip:Hide()
            end)
            yOffset = yOffset + FW.VENDOR_LIST_VENDOR_ROW_HEIGHT

            -- Item rows (only if expanded)
            if isExpanded then
                -- Sort items by name
                table.sort(vData.items, function(a, b)
                    local na = HDG_Data and HDG_Data.GetCachedItemName(a.itemID) or ""
                    local nb = HDG_Data and HDG_Data.GetCachedItemName(b.itemID) or ""
                    return na < nb
                end)

                for _, entry in ipairs(vData.items) do
                    local iRow = GetPooledRow(scrollContent, FW.VENDOR_LIST_ITEM_ROW_HEIGHT)
                    iRow:SetPoint("TOPLEFT", FW.VENDOR_LIST_INDENT_ITEM, -yOffset)
                    iRow:SetPoint("RIGHT", scrollContent, "RIGHT", 0, 0)
                    if scheme then
                        iRow:SetBackdropColor(scheme.panel.r, scheme.panel.g, scheme.panel.b, 0.3)
                        iRow:SetBackdropBorderColor(0, 0, 0, 0)
                    end

                    -- Icon
                    iRow.iconTex:SetSize(FW.VENDOR_LIST_ITEM_ICON_SIZE, FW.VENDOR_LIST_ITEM_ICON_SIZE)
                    iRow.iconTex:Show()
                    local item = Item:CreateFromItemID(entry.itemID)
                    local iRowRef = iRow
                    item:ContinueOnItemLoad(function()
                        if iRowRef.inUse then
                            local _, _, _, _, _, _, _, _, _, ic = C_Item.GetItemInfo(entry.itemID)
                            if ic then iRowRef.iconTex:SetTexture(ic) end
                        end
                    end)

                    -- Name
                    local itemName = HDG_Data and HDG_Data.GetCachedItemName(entry.itemID) or ("Item " .. entry.itemID)
                    iRow.nameText:ClearAllPoints()
                    iRow.nameText:SetPoint("LEFT", iRow.iconTex, "RIGHT", 4, 0)
                    iRow.nameText:SetWidth(contentWidth - FW.VENDOR_LIST_INDENT_ITEM - 70)
                    iRow.nameText:SetText(itemName)
                    if scheme then ApplyFont(iRow.nameText, scheme, "small") end

                    -- Gold cost (right side, before remove button)
                    local itemEntry = ve and ve.items and ve.items[entry.itemID]
                    local goldCost = itemEntry and itemEntry[1] or 0
                    iRow.rightText:ClearAllPoints()
                    iRow.rightText:SetPoint("RIGHT", -18, 0)
                    iRow.rightText:SetText(FormatGoldCompact(goldCost))
                    if scheme then ApplyFont(iRow.rightText, scheme, "small") end

                    -- Rep badge
                    local repReq = itemEntry and itemEntry[2] and itemEntry[2] > 0
                    if repReq then
                        iRow.badgeText:ClearAllPoints()
                        iRow.badgeText:SetPoint("LEFT", iRow.nameText, "RIGHT", 2, 0)
                        iRow.badgeText:SetText(C_Dim() .. "Rep|r")
                        iRow.badgeText:Show()
                    end

                    -- Remove button
                    iRow.removeBtn:Show()
                    local entryItemID, entryNpcID = entry.itemID, entry.npcID
                    iRow.removeBtn:SetScript("OnClick", function()
                        if HDG.Store then
                            HDG.Store:RemoveFromVendorShoppingList(entryItemID, entryNpcID)
                            HDG_VendorShoppingList.Refresh()
                        end
                    end)

                    -- Tooltip
                    local eItemID = entry.itemID
                    iRow:SetScript("OnEnter", function(self)
                        local c = GetScheme()
                        if c then self:SetBackdropColor(c.button_hover.r, c.button_hover.g, c.button_hover.b, 0.3) end
                        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
                        local itm = Item:CreateFromItemID(eItemID)
                        itm:ContinueOnItemLoad(function()
                            if GameTooltip:IsOwned(self) then
                                GameTooltip:SetItemByID(eItemID)
                                GameTooltip:Show()
                            end
                        end)
                    end)
                    iRow:SetScript("OnLeave", function(self)
                        local c = GetScheme()
                        if c then self:SetBackdropColor(c.panel.r, c.panel.g, c.panel.b, 0.3) end
                        GameTooltip:Hide()
                    end)

                    yOffset = yOffset + FW.VENDOR_LIST_ITEM_ROW_HEIGHT
                end
            end
        end
    end

    scrollContent:SetHeight(math.max(yOffset, 1))
end

-- ============================================================================
-- ZONE MODE
-- ============================================================================

local function RefreshZoneMode()
    local FW = CONFIG.FLOATING_WIDGET

    currentZoneMapID = currentZoneMapID or C_Map.GetBestMapForUnit("player")
    currentZoneVendors = HDG_VendorResolver.GetVendorsInZoneWithUncollected(currentZoneMapID)

    ReleaseAllRows()

    if not currentZoneVendors or #currentZoneVendors == 0 then
        local row = GetPooledRow(scrollContent, 40)
        row:SetPoint("TOPLEFT", 0, 0)
        row:SetPoint("RIGHT", scrollContent, "RIGHT", 0, 0)
        row:SetBackdropColor(0, 0, 0, 0)
        row:SetBackdropBorderColor(0, 0, 0, 0)
        row.nameText:ClearAllPoints()
        row.nameText:SetPoint("LEFT", 8, 0)
        row.nameText:SetWidth(scrollContent:GetWidth() - 16)
        row.nameText:SetText(C_Dim() .. "No vendors with uncollected items\nin this zone.|r")
        row.nameText:SetWordWrap(true)
        scrollContent:SetHeight(40)
        return
    end

    -- Sort vendors by name
    table.sort(currentZoneVendors, function(a, b) return (a.name or "") < (b.name or "") end)

    local yOffset = 0
    local contentWidth = scrollContent:GetWidth()
    local scheme = GetScheme()

    -- Zone header
    local totalItems = 0
    for _, v in ipairs(currentZoneVendors) do totalItems = totalItems + #v.uncollectedItems end
    local zoneName = currentZoneVendors[1] and currentZoneVendors[1].zone or "Unknown"

    local zRow = GetPooledRow(scrollContent, FW.VENDOR_LIST_ZONE_HEADER_HEIGHT)
    zRow:SetPoint("TOPLEFT", 0, 0)
    zRow:SetPoint("RIGHT", scrollContent, "RIGHT", 0, 0)
    if scheme then
        zRow:SetBackdropColor(scheme.accent.r, scheme.accent.g, scheme.accent.b, 0.15)
        zRow:SetBackdropBorderColor(scheme.accent.r, scheme.accent.g, scheme.accent.b, 0.3)
    end
    zRow.nameText:ClearAllPoints()
    zRow.nameText:SetPoint("LEFT", 6, 0)
    zRow.nameText:SetWidth(contentWidth - 10)
    zRow.nameText:SetText("|cFFe2e8f0" .. zoneName .. "|r  " .. C_Dim() .. #currentZoneVendors .. " vendors, " .. totalItems .. " uncollected|r")
    if scheme then ApplyFont(zRow.nameText, scheme, "body") end
    yOffset = yOffset + FW.VENDOR_LIST_ZONE_HEADER_HEIGHT

    for _, vendor in ipairs(currentZoneVendors) do
        local isExpanded = expandedVendors[vendor.npcID]

        -- Vendor row
        local vRow = GetPooledRow(scrollContent, FW.VENDOR_LIST_VENDOR_ROW_HEIGHT)
        vRow:SetPoint("TOPLEFT", FW.VENDOR_LIST_INDENT_VENDOR, -yOffset)
        vRow:SetPoint("RIGHT", scrollContent, "RIGHT", 0, 0)
        if scheme then
            vRow:SetBackdropColor(scheme.bg.r, scheme.bg.g, scheme.bg.b, 0.3)
            vRow:SetBackdropBorderColor(0, 0, 0, 0)
        end

        -- Expand arrow
        vRow.expandArrow:ClearAllPoints()
        vRow.expandArrow:SetPoint("LEFT", 2, 0)
        vRow.expandArrow:SetText(isExpanded and C_Dim() .. "v|r" or C_Dim() .. ">|r")
        vRow.expandArrow:Show()

        vRow.nameText:ClearAllPoints()
        vRow.nameText:SetPoint("LEFT", 14, 0)
        vRow.nameText:SetWidth(contentWidth - FW.VENDOR_LIST_INDENT_VENDOR - 80)
        vRow.nameText:SetText(C_Accent() .. vendor.name .. "|r")
        if scheme then ApplyFont(vRow.nameText, scheme, "body") end

        -- Count badge
        vRow.badgeText:ClearAllPoints()
        vRow.badgeText:SetPoint("LEFT", vRow.nameText, "RIGHT", 4, 0)
        vRow.badgeText:SetText(C_Dim() .. "(" .. #vendor.uncollectedItems .. ")|r")
        vRow.badgeText:Show()

        -- Coords
        if vendor.x and vendor.x > 0 then
            vRow.rightText:SetText(C_Dim() .. string.format("%.0f, %.0f", vendor.x, vendor.y) .. "|r")
            if scheme then ApplyFont(vRow.rightText, scheme, "small") end
        end

        -- Click → toggle expand, shift-click → waypoint or add all
        local vRef = vendor
        vRow:SetScript("OnClick", function()
            if IsShiftKeyDown() then
                -- Shift-click: add all uncollected items from vendor
                local count = 0
                for _, ui in ipairs(vRef.uncollectedItems) do
                    if HDG.Store:AddToVendorShoppingList(ui.itemID, vRef.npcID) then
                        count = count + 1
                    end
                end
                if count > 0 then
                    print(C_Success() .. "[HDG]|r Added " .. C_Accent() .. count .. "|r items from " .. vRef.name .. " to shopping list")
                    HDG_VendorShoppingList.Refresh()
                end
            else
                expandedVendors[vRef.npcID] = not expandedVendors[vRef.npcID]
                HDG_VendorShoppingList.Refresh()
            end
        end)
        vRow:SetScript("OnEnter", function(self)
            local c = GetScheme()
            if c then self:SetBackdropColor(c.button_hover.r, c.button_hover.g, c.button_hover.b, 0.3) end
            GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
            GameTooltip:SetText(vRef.name, 1, 1, 1)
            if vRef.x and vRef.x > 0 then
                GameTooltip:AddLine(string.format("%.1f, %.1f", vRef.x, vRef.y), 0.7, 0.7, 0.7)
            end
            GameTooltip:AddLine(" ")
            GameTooltip:AddLine("Click to expand/collapse", 0.5, 0.5, 0.5)
            GameTooltip:AddLine("Shift-Click to add all uncollected items", 0.5, 0.5, 0.5)
            GameTooltip:Show()
        end)
        vRow:SetScript("OnLeave", function(self)
            local c = GetScheme()
            if c then self:SetBackdropColor(c.bg.r, c.bg.g, c.bg.b, 0.3) end
            GameTooltip:Hide()
        end)
        yOffset = yOffset + FW.VENDOR_LIST_VENDOR_ROW_HEIGHT

        -- Item rows (only if expanded)
        if isExpanded then
            -- Sort items by name
            table.sort(vendor.uncollectedItems, function(a, b)
                local na = HDG_Data and HDG_Data.GetCachedItemName(a.itemID) or ""
                local nb = HDG_Data and HDG_Data.GetCachedItemName(b.itemID) or ""
                return na < nb
            end)

            for _, ui in ipairs(vendor.uncollectedItems) do
                local iRow = GetPooledRow(scrollContent, FW.VENDOR_LIST_ITEM_ROW_HEIGHT)
                iRow:SetPoint("TOPLEFT", FW.VENDOR_LIST_INDENT_ITEM, -yOffset)
                iRow:SetPoint("RIGHT", scrollContent, "RIGHT", 0, 0)
                if scheme then
                    iRow:SetBackdropColor(scheme.panel.r, scheme.panel.g, scheme.panel.b, 0.3)
                    iRow:SetBackdropBorderColor(0, 0, 0, 0)
                end

                -- Icon
                iRow.iconTex:SetSize(FW.VENDOR_LIST_ITEM_ICON_SIZE, FW.VENDOR_LIST_ITEM_ICON_SIZE)
                iRow.iconTex:Show()
                local itm = Item:CreateFromItemID(ui.itemID)
                local iRowRef = iRow
                itm:ContinueOnItemLoad(function()
                    if iRowRef.inUse then
                        local _, _, _, _, _, _, _, _, _, ic = C_Item.GetItemInfo(ui.itemID)
                        if ic then iRowRef.iconTex:SetTexture(ic) end
                    end
                end)

                -- Name
                local itemName = HDG_Data and HDG_Data.GetCachedItemName(ui.itemID) or ("Item " .. ui.itemID)
                iRow.nameText:ClearAllPoints()
                iRow.nameText:SetPoint("LEFT", iRow.iconTex, "RIGHT", 4, 0)
                iRow.nameText:SetWidth(contentWidth - FW.VENDOR_LIST_INDENT_ITEM - 50)
                iRow.nameText:SetText(itemName)
                if scheme then ApplyFont(iRow.nameText, scheme, "small") end

                -- Gold cost
                iRow.rightText:ClearAllPoints()
                iRow.rightText:SetPoint("RIGHT", -4, 0)
                iRow.rightText:SetText(FormatGoldCompact(ui.goldCost))
                if scheme then ApplyFont(iRow.rightText, scheme, "small") end

                -- Shift-click: add to list
                local uiItemID = ui.itemID
                local vNpcID = vendor.npcID
                iRow:SetScript("OnClick", function()
                    if IsShiftKeyDown() then
                        if HDG.Store:AddToVendorShoppingList(uiItemID, vNpcID) then
                            local name = HDG_Data and HDG_Data.GetCachedItemName(uiItemID) or ("Item " .. uiItemID)
                            print(C_Success() .. "[HDG]|r Added " .. C_Accent() .. name .. "|r to shopping list")
                        end
                    end
                end)
                iRow:SetScript("OnEnter", function(self)
                    local c = GetScheme()
                    if c then self:SetBackdropColor(c.button_hover.r, c.button_hover.g, c.button_hover.b, 0.3) end
                    GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
                    local it = Item:CreateFromItemID(uiItemID)
                    it:ContinueOnItemLoad(function()
                        if GameTooltip:IsOwned(self) then
                            GameTooltip:SetItemByID(uiItemID)
                            GameTooltip:AddLine(" ")
                            GameTooltip:AddLine("Shift-Click to add to Shopping List", 0.5, 0.5, 0.5)
                            GameTooltip:Show()
                        end
                    end)
                end)
                iRow:SetScript("OnLeave", function(self)
                    local c = GetScheme()
                    if c then self:SetBackdropColor(c.panel.r, c.panel.g, c.panel.b, 0.3) end
                    GameTooltip:Hide()
                end)

                yOffset = yOffset + FW.VENDOR_LIST_ITEM_ROW_HEIGHT
            end
        end
    end

    scrollContent:SetHeight(math.max(yOffset, 1))
end

-- ============================================================================
-- WAYPOINT MAPPING
-- ============================================================================

function HDG_VendorShoppingList.MapListWaypoints()
    if not HDG.Store then return end
    local list = HDG.Store:GetState().vendorShoppingList
    local seen = {}
    local count = 0
    for _, entry in ipairs(list) do
        if not seen[entry.npcID] then
            seen[entry.npcID] = true
            local ve = HDG_VendorDB and HDG_VendorDB[entry.npcID]
            if ve then
                local name = HDG_VendorResolver.GetVendorNameByID(entry.npcID)
                if SetWaypoint(ve[3], ve[4], ve[5], name) then
                    count = count + 1
                end
            end
        end
    end
    if count > 0 then
        print(string.format(C_Success() .. "[HDG]|r Set %d waypoints for %d vendors.", count, count))
    else
        print("|cFFFFAA00[HDG]|r No valid vendor locations to map.")
    end
end

function HDG_VendorShoppingList.MapZoneWaypoints()
    if not currentZoneVendors then return end
    local count = 0
    for _, vendor in ipairs(currentZoneVendors) do
        if SetWaypoint(vendor.mapID, vendor.x, vendor.y, vendor.name) then
            count = count + 1
        end
    end
    if count > 0 then
        print(string.format(C_Success() .. "[HDG]|r Set %d waypoints for zone vendors.", count))
    else
        print("|cFFFFAA00[HDG]|r No valid vendor locations to map.")
    end
end

-- ============================================================================
-- PUBLIC API
-- ============================================================================

function HDG_VendorShoppingList:Init(configRef)
    CONFIG = configRef
end

function HDG_VendorShoppingList.Toggle(mode)
    if not CONFIG then CONFIG = privateDB.CONFIG end
    if not created then CreatePanel() end

    if widget.frame:IsShown() and (not mode or mode == activeMode) then
        widget.frame:Hide()
    else
        if mode then activeMode = mode end
        widget.frame:Show()
        HDG_VendorShoppingList.Refresh()
    end
end

function HDG_VendorShoppingList.Show(mode)
    if not CONFIG then CONFIG = privateDB.CONFIG end
    if not created then CreatePanel() end
    if mode then activeMode = mode end
    widget.frame:Show()
    HDG_VendorShoppingList.Refresh()
end

function HDG_VendorShoppingList.Hide()
    if widget and widget.frame then widget.frame:Hide() end
end

function HDG_VendorShoppingList.IsShown()
    return widget and widget.frame and widget.frame:IsShown()
end

function HDG_VendorShoppingList.GetCount()
    if not HDG.Store then return 0 end
    return HDG.Store:GetVendorShoppingListCount()
end

function HDG_VendorShoppingList.Refresh()
    if not widget or not widget.frame:IsShown() then return end

    UpdateModeHighlights()

    -- Show/hide mode-specific elements
    local isListMode = activeMode == "list"
    local isZoneMode = activeMode == "zone"
    local isConfigMode = activeMode == "config"

    scrollFrame:SetShown(isListMode or isZoneMode)
    configContent:SetShown(isConfigMode)

    widget.clearBtn:SetShown(isListMode)
    widget.mapBtn:SetShown(isListMode or isZoneMode)

    -- Refresh config checkboxes
    if isConfigMode and widget.cbZoneScanner then
        widget.cbZoneScanner:SetChecked(GetConfig("zoneScannerEnabled") or false)
        widget.cbZoneSound:SetChecked(GetConfig("zoneScannerSound") or false)
    end

    if isListMode then
        RefreshListMode()
    elseif isZoneMode then
        RefreshZoneMode()
    end
end

function HDG_VendorShoppingList.OnZoneChanged(mapID)
    currentZoneMapID = mapID
    currentZoneVendors = nil -- invalidate cache

    -- Auto-open in zone mode if enabled + zone has uncollected vendor items
    if GetConfig("zoneScannerEnabled")
       and not InCombatLockdown()
       and HDG_VendorResolver.ZoneHasUncollectedVendorItems(mapID) then
        HDG_VendorShoppingList.Show("zone")
        if GetConfig("zoneScannerSound") then
            PlaySound(SOUNDKIT.IG_MAINMENU_OPEN)
        end
    end

    -- If already showing in zone mode, just refresh
    if HDG_VendorShoppingList.IsShown() and activeMode == "zone" then
        HDG_VendorShoppingList.Refresh()
    end
end
