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", "style", "config"
local currentZoneMapID = nil
local currentZoneVendors = nil
local rowPool = {}
local modeButtons = {}
local expandedVendors = {} -- [npcID] = true for collapsed/expanded state
local expandedStyles = {}  -- [styleId] = true for style mode expand/collapse

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, so) if HDG.Theme then HDG.Theme.ApplyFont(fs, s, sz, so) end 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 "|cFFfcd34d" .. gold .. "g|r" end
    local silver = math.floor((copper % 10000) / 100)
    return HDG.Theme.C_Dim() .. silver .. "s|r"
end

local function SetWaypoint(mapID, x, y, title)
    local HDG = _G.HousingDecorGuide
    if not HDG or not HDG.Waypoints then return false end
    local ok, provider = HDG.Waypoints:Set(mapID, x, y, title)
    return ok, provider
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)

    -- 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, -11)
    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, -10)
    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
    local closeBtnSize = FW.HEADER_BTN_SIZE - 5

    -- Close button (repositioned to tab bar below)
    local closeBtn = widget.CreateHeaderButton(closeBtnSize, "Close")
    closeBtn:SetBackdropColor(0, 0, 0, 0)
    closeBtn:SetBackdropBorderColor(0, 0, 0, 0)
    closeBtn:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, HDG_UIHelpers and HDG_UIHelpers.TOOLTIP_ANCHOR or "ANCHOR_RIGHT")
        GameTooltip:SetText("Close", 1, 1, 1)
        GameTooltip:Show()
    end)
    closeBtn:SetScript("OnLeave", function() GameTooltip:Hide() end)
    local closeTex = closeBtn:CreateTexture(nil, "ARTWORK")
    closeTex:SetSize(16, 16)
    closeTex:SetPoint("CENTER")
    closeTex:SetAtlas("XMarksTheSpot")
    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")
    clearBtn:SetBackdropColor(0, 0, 0, 0)
    clearBtn:SetBackdropBorderColor(0, 0, 0, 0)
    clearBtn:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, HDG_UIHelpers and HDG_UIHelpers.TOOLTIP_ANCHOR or "ANCHOR_RIGHT")
        GameTooltip:SetText("Clear All", 1, 1, 1)
        GameTooltip:Show()
    end)
    clearBtn:SetScript("OnLeave", function() GameTooltip:Hide() end)
    local clearIcon = clearBtn:CreateTexture(nil, "ARTWORK")
    clearIcon:SetSize(15, 15)
    clearIcon:SetPoint("CENTER")
    clearIcon:SetAtlas("common-icon-delete")
    clearBtn:SetScript("OnClick", function()
        if HDG.Store then
            if activeMode == "style" then
                HDG.Store:ClearStyleShoppingList()
            else
                HDG.Store:ClearVendorShoppingList()
            end
            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 = "style", label = "Style" }, { key = "config", label = "Config" } }
    local btnW = 55
    local btnGap = 2
    local iconBtnW = closeBtnSize + 4 -- icon button width + gap
    local totalTabW = #modes * btnW + (#modes - 1) * btnGap
    local totalBarW = iconBtnW + totalTabW + iconBtnW
    local barStartX = math.floor((FW.VENDOR_LIST_WIDTH - 8 - totalBarW) / 2)

    -- Clear All button (left of tabs, above border)
    clearBtn:ClearAllPoints()
    clearBtn:SetSize(closeBtnSize, FW.VENDOR_LIST_MODE_BAR_HEIGHT - 2)
    clearBtn:SetPoint("TOPLEFT", modeBar, "TOPLEFT", barStartX, -1)
    clearBtn:SetFrameLevel(frame:GetFrameLevel() + 25)
    HDG_UIHelpers.SetupTooltip(clearBtn, "Clear All", "Remove all items from the shopping list.", "ANCHOR_BOTTOM")

    local tabStartX = barStartX + iconBtnW
    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

    -- Close button (right of tabs, above border)
    closeBtn:ClearAllPoints()
    closeBtn:SetSize(closeBtnSize, FW.VENDOR_LIST_MODE_BAR_HEIGHT - 2)
    closeBtn:SetPoint("TOPLEFT", modeBar, "TOPLEFT", tabStartX + totalTabW + 4, -1)
    closeBtn:SetFrameLevel(frame:GetFrameLevel() + 25)

    -- ====================================================================
    -- MAP WAYPOINTS ROW (below mode tabs)
    -- ====================================================================
    local mapRowY = modeBarY - FW.VENDOR_LIST_MODE_BAR_HEIGHT - 2
    local mapRowH = 22
    local mapRowW = FW.VENDOR_LIST_WIDTH - 16
    local mapW = math.floor((mapRowW - 4) * 2 / 3)
    local clearW = mapRowW - 4 - mapW

    local mapBtn = HDG.UI:Button(frame, "Map All", mapW, mapRowH, function()
        if activeMode == "list" then
            HDG_VendorShoppingList.MapListWaypoints()
        elseif activeMode == "zone" then
            HDG_VendorShoppingList.MapZoneWaypoints()
        end
    end)
    mapBtn:SetPoint("TOPLEFT", 8, mapRowY)
    local mapIcon = mapBtn:CreateTexture(nil, "ARTWORK")
    mapIcon:SetSize(12, 12)
    mapIcon:SetPoint("LEFT", 6, 0)
    mapIcon:SetAtlas("Waypoint-MapPin-ChatIcon")
    widget.mapBtn = mapBtn

    local clearPinsBtn = HDG.UI:Button(frame, "Clear Pins", clearW, mapRowH, function()
        if HDG.Waypoints then
            HDG.Waypoints:Clear()
            print("|cFF88BBDD[HDG]|r All waypoints and map pins cleared.")
        end
    end)
    clearPinsBtn:SetPoint("TOPRIGHT", -8, mapRowY)
    local clearIcon = clearPinsBtn:CreateTexture(nil, "ARTWORK")
    clearIcon:SetSize(12, 12)
    clearIcon:SetPoint("LEFT", 6, 0)
    clearIcon:SetAtlas("common-icon-delete")
    widget.clearPinsBtn = clearPinsBtn

    -- (Clear All button positioned in tab bar section above)

    -- ====================================================================
    -- 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 lastAnchor = configContent -- previous element to anchor below
    local lastIsTop = true -- first anchor uses TOPLEFT, subsequent use BOTTOMLEFT

    local function MakeCheckbox(label, hint, configKey, indent)
        local xOff = 8 + (indent or 0)
        local cb = CreateFrame("CheckButton", nil, configContent, "UICheckButtonTemplate")
        cb:SetSize(26, 26)
        cb:SetPoint("LEFT", configContent, "LEFT", xOff, 0)
        if lastIsTop then
            cb:SetPoint("TOP", lastAnchor, "TOP", 0, -8)
        else
            cb:SetPoint("TOP", lastAnchor, "BOTTOM", 0, -6)
        end
        cb:SetChecked(GetConfig(configKey) or false)

        local cbLabel = configContent:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        cbLabel:SetPoint("LEFT", cb, "RIGHT", 4, 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", cbLabel, "BOTTOMLEFT", 0, -1)
            hintLabel:SetPoint("RIGHT", configContent, "RIGHT", -8, 0)
            if scheme then ApplyFont(hintLabel, scheme, "small") end
            hintLabel:SetText("|cFFb0b8c8" .. hint .. "|r")
            hintLabel:SetWordWrap(true)
            hintLabel:SetJustifyH("LEFT")
            lastAnchor = hintLabel
        else
            lastAnchor = cbLabel
        end
        lastIsTop = false

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

        return cb
    end

    widget.cbZoneScanner = MakeCheckbox(
        "Zone Scanner",
        "Detect uncollected vendor items and shopping list matches on zone change",
        "zoneScannerEnabled"
    )
    widget.cbZonePopup = MakeCheckbox(
        "Pop Up Shopping List",
        "Auto-open the shopping list window when entering a zone",
        "zoneScannerPopup", 6
    )
    widget.cbZoneSquirrels = MakeCheckbox(
        "Squirrels",
        "Squirrel talking head popup for zone alerts",
        "zoneScannerSquirrels", 6
    )
    widget.cbZoneChat = MakeCheckbox(
        "Chat Messages",
        "Print zone alerts to chat window",
        "zoneScannerChat", 6
    )
    widget.cbZoneSound = MakeCheckbox(
        "Sound",
        "Play notification sound when zone scanner triggers",
        "zoneScannerSound", 6
    )

    -- Master toggle disables sub-options
    local subOptions = {widget.cbZonePopup, widget.cbZoneSquirrels, widget.cbZoneChat, widget.cbZoneSound}
    local function UpdateScannerSubOptions()
        local enabled = GetConfig("zoneScannerEnabled") or false
        local alpha = enabled and 1.0 or 0.35
        for _, cb in ipairs(subOptions) do
            cb:SetAlpha(alpha)
            cb:SetEnabled(enabled)
        end
    end
    local origScannerClick = widget.cbZoneScanner:GetScript("OnClick")
    widget.cbZoneScanner:SetScript("OnClick", function(self)
        origScannerClick(self)
        UpdateScannerSubOptions()
    end)
    widget._updateScannerSubOptions = UpdateScannerSubOptions

    -- ====================================================================
    -- EXPORT / IMPORT BUTTONS
    -- ====================================================================
    local btnW = (configWidth - 16) / 2

    -- Vendor list export/import
    local vendorLabel = configContent:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    vendorLabel:SetPoint("LEFT", configContent, "LEFT", 8, 0)
    vendorLabel:SetPoint("TOP", lastAnchor, "BOTTOM", 0, -12)
    if scheme then ApplyFont(vendorLabel, scheme, "small") end
    vendorLabel:SetText("|cFFe2e8f0Vendor List|r")

    local vendorExport = HDG.UI:Button(configContent, "Export", btnW, 20, function()
        HDG_VendorShoppingList.ShowExportImport("export", "vendor")
    end)
    vendorExport:SetPoint("TOPLEFT", vendorLabel, "BOTTOMLEFT", 0, -4)

    local vendorImport = HDG.UI:Button(configContent, "Import", btnW, 20, function()
        HDG_VendorShoppingList.ShowExportImport("import", "vendor")
    end)
    vendorImport:SetPoint("LEFT", vendorExport, "RIGHT", 4, 0)

    -- Style list export/import
    local styleLabel = configContent:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    styleLabel:SetPoint("TOPLEFT", vendorExport, "BOTTOMLEFT", 0, -10)
    if scheme then ApplyFont(styleLabel, scheme, "small") end
    styleLabel:SetText("|cFFe2e8f0Style List|r")

    local styleExport = HDG.UI:Button(configContent, "Export", btnW, 20, function()
        HDG_VendorShoppingList.ShowExportImport("export", "style")
    end)
    styleExport:SetPoint("TOPLEFT", styleLabel, "BOTTOMLEFT", 0, -4)

    local styleImport = HDG.UI:Button(configContent, "Import", btnW, 20, function()
        HDG_VendorShoppingList.ShowExportImport("import", "style")
    end)
    styleImport:SetPoint("LEFT", styleExport, "RIGHT", 4, 0)
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

local function GetSourceBadge(sourceType)
    local labels = { [1] = "Ach", [2] = "Q", [3] = "WQ", [4] = "Drop", [5] = "Vendor", [6] = "Craft",
        [7] = "Learn", [8] = "Gather", [9] = "Treas", [10] = "Promo", [11] = "Prof" }
    return labels[sourceType] or ""
end

local function SearchAuctionHouse(itemName)
    if not itemName or itemName == "" then return end
    if AuctionHouseFrame and AuctionHouseFrame:IsShown() then
        local searchBox = AuctionHouseFrame.SearchBar and AuctionHouseFrame.SearchBar.SearchBox
        if searchBox then
            searchBox:SetText(itemName)
            if AuctionHouseFrame.SearchBar.SearchButton then
                AuctionHouseFrame.SearchBar.SearchButton:Click()
            end
        end
    else
        print(HDG.Theme.C_Success() .. "Housing Decor Guide:|r Open the Auction House to search")
    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(HDG.Theme.C_Dim() .. "No items on your list.\nShift-Click items to add them.|r")
        row.nameText:SetWordWrap(true)
        scrollContent:SetHeight(40)
        return
    end

    -- Build zone → vendor → items hierarchy + wish list for nil-npcID entries
    local zones = {}
    local zoneOrder = {}
    local wishListItems = {} -- entries with nil npcID
    for _, entry in ipairs(list) do
        if not entry.npcID then
            table.insert(wishListItems, entry)
        else
            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, currencies = {} }
            end
            local ie = ve and ve.items and ve.items[entry.itemID]
            local goldCost = ie and ie[1] or 0
            table.insert(zones[zoneName].vendors[npcID].items, entry)
            zones[zoneName].vendors[npcID].totalGold = zones[zoneName].vendors[npcID].totalGold + goldCost
            if ie and ie[5] then
                for _, ct in ipairs(ie[5]) do
                    if ct[1] == "c" then
                        local cid = ct[2]
                        zones[zoneName].vendors[npcID].currencies[cid] = (zones[zoneName].vendors[npcID].currencies[cid] or 0) + ct[3]
                    end
                end
            end
        end
    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 totals (gold + currencies)
        local zoneTotalGold = 0
        local zoneCurrencies = {}
        for _, vd in pairs(zoneData.vendors) do
            zoneTotalGold = zoneTotalGold + vd.totalGold
            for cid, amt in pairs(vd.currencies) do
                zoneCurrencies[cid] = (zoneCurrencies[cid] or 0) + amt
            end
        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", 13) end
        local costParts = {}
        if zoneTotalGold > 0 then table.insert(costParts, FormatGoldCompact(zoneTotalGold)) end
        for cid, amt in pairs(zoneCurrencies) do
            local info = C_CurrencyInfo and C_CurrencyInfo.GetCurrencyInfo(cid)
            local icon = info and info.iconFileID
            local iconStr = icon and string.format("|T%s:12:12|t", icon) or ""
            table.insert(costParts, iconStr .. HDG.Theme.C_Accent() .. amt .. "|r")
        end
        zRow.rightText:SetText(table.concat(costParts, " "))
        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 HDG.Theme.C_Dim() .. "v|r" or HDG.Theme.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(HDG.Theme.C_Accent() .. vendorName .. "|r")
            if scheme then ApplyFont(vRow.nameText, scheme, "body", 13) end

            -- Item count badge
            vRow.badgeText:ClearAllPoints()
            vRow.badgeText:SetPoint("LEFT", vRow.nameText, "RIGHT", 4, 0)
            vRow.badgeText:SetText(HDG.Theme.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
                        local ok, provider = SetWaypoint(ve[3], ve[4], ve[5], vendorName)
                        if ok then
                            HDG.Waypoints:PrintResult(true, provider, vendorName, nil, ve[3], ve[6])
                        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.78, 0.78, 0.78)
                    end
                end
                GameTooltip:AddLine(" ")
                GameTooltip:AddLine("Click to expand/collapse", 0.78, 0.78, 0.78)
                GameTooltip:AddLine("Shift-Click to set waypoint", 0.78, 0.78, 0.78)
                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 (with qty prefix)
                    local itemName = HDG_Data and HDG_Data.GetCachedItemName(entry.itemID) or ("Item " .. entry.itemID)
                    local qty = entry.qty or 1
                    local qtyPrefix = qty > 1 and (HDG.Theme.C_Accent() .. qty .. "x|r ") or ""
                    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(qtyPrefix .. itemName)
                    if scheme then ApplyFont(iRow.nameText, scheme, "small") end

                    local itemEntry = ve and ve.items and ve.items[entry.itemID]

                    -- Gate badge (Rep/Quest/Achieve)
                    local repReq = itemEntry and itemEntry[2] and itemEntry[2] > 0
                    local questReq = itemEntry and itemEntry[6] and itemEntry[6] > 0
                    local achieveReq = itemEntry and itemEntry[7] and itemEntry[7] > 0
                    if repReq or questReq or achieveReq then
                        local badge = repReq and "Rep" or (questReq and "Quest" or "Achieve")
                        iRow.badgeText:ClearAllPoints()
                        iRow.badgeText:SetPoint("LEFT", iRow.nameText, "RIGHT", 2, 0)
                        iRow.badgeText:SetText(HDG.Theme.C_Dim() .. badge .. "|r")
                        iRow.badgeText:Show()
                    end

                    -- Remove button (decrements qty, removes at 0)
                    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 (shows item + cost breakdown)
                    local eItemID = entry.itemID
                    local eItemEntry = itemEntry
                    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)
                                if eItemEntry then
                                    local gc = eItemEntry[1] or 0
                                    if gc > 0 then
                                        GameTooltip:AddLine("Cost: " .. GetCoinTextureString(gc), 1, 1, 1)
                                    end
                                    if eItemEntry[5] then
                                        for _, ct in ipairs(eItemEntry[5]) do
                                            if ct[1] == "c" then
                                                local info = C_CurrencyInfo and C_CurrencyInfo.GetCurrencyInfo(ct[2])
                                                local cName = info and info.name or ("Currency " .. ct[2])
                                                local icon = info and info.iconFileID
                                                local iconStr = icon and string.format("|T%s:14:14|t", icon) or ""
                                                GameTooltip:AddLine(iconStr .. " " .. ct[3] .. " " .. cName, 0.7, 0.8, 1.0)
                                            end
                                        end
                                    end
                                end
                                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)

                    -- Shift-click: search AH for craftable items
                    local srcType = HDG_AllDecorDB and HDG_AllDecorDB[entry.itemID] and HDG_AllDecorDB[entry.itemID][2] or 0
                    if srcType == 6 then
                        local eItemName = itemName
                        iRow:SetScript("OnClick", function()
                            if IsShiftKeyDown() then SearchAuctionHouse(eItemName) end
                        end)
                    end

                    yOffset = yOffset + FW.VENDOR_LIST_ITEM_ROW_HEIGHT
                end
            end
        end
    end

    -- Wish List section (non-vendor items)
    if #wishListItems > 0 then
        -- Header row
        local wRow = GetPooledRow(scrollContent, FW.VENDOR_LIST_ZONE_HEADER_HEIGHT)
        wRow:SetPoint("TOPLEFT", 0, -yOffset)
        wRow:SetPoint("RIGHT", scrollContent, "RIGHT", 0, 0)
        if scheme then
            wRow:SetBackdropColor(scheme.accent.r, scheme.accent.g, scheme.accent.b, 0.15)
            wRow:SetBackdropBorderColor(scheme.accent.r, scheme.accent.g, scheme.accent.b, 0.3)
        end
        wRow.nameText:ClearAllPoints()
        wRow.nameText:SetPoint("LEFT", 6, 0)
        wRow.nameText:SetWidth(contentWidth - 60)
        wRow.nameText:SetText("|cFFe2e8f0Wish List|r")
        if scheme then ApplyFont(wRow.nameText, scheme, "body", 13) end
        local wishTotal = 0
        for _, we in ipairs(wishListItems) do wishTotal = wishTotal + (we.qty or 1) end
        wRow.rightText:SetText(HDG.Theme.C_Dim() .. wishTotal .. " items|r")
        if scheme then ApplyFont(wRow.rightText, scheme, "small") end
        yOffset = yOffset + FW.VENDOR_LIST_ZONE_HEADER_HEIGHT

        -- Sort wish list items by name
        table.sort(wishListItems, 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(wishListItems) do
            local iRow = GetPooledRow(scrollContent, FW.VENDOR_LIST_ITEM_ROW_HEIGHT)
            iRow:SetPoint("TOPLEFT", FW.VENDOR_LIST_INDENT_VENDOR, -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 (with qty prefix)
            local itemName = HDG_Data and HDG_Data.GetCachedItemName(entry.itemID) or ("Item " .. entry.itemID)
            local qty = entry.qty or 1
            local qtyPrefix = qty > 1 and (HDG.Theme.C_Accent() .. qty .. "x|r ") or ""
            iRow.nameText:ClearAllPoints()
            iRow.nameText:SetPoint("LEFT", iRow.iconTex, "RIGHT", 4, 0)
            iRow.nameText:SetWidth(contentWidth - FW.VENDOR_LIST_INDENT_VENDOR - 70)
            iRow.nameText:SetText(qtyPrefix .. itemName)
            if scheme then ApplyFont(iRow.nameText, scheme, "small") end

            -- Source badge (Craft/Drop/AH etc)
            local srcType = HDG_AllDecorDB and HDG_AllDecorDB[entry.itemID] and HDG_AllDecorDB[entry.itemID][2] or 0
            local badge = GetSourceBadge(srcType)
            if badge ~= "" then
                iRow.badgeText:ClearAllPoints()
                iRow.badgeText:SetPoint("LEFT", iRow.nameText, "RIGHT", 2, 0)
                iRow.badgeText:SetText(HDG.Theme.C_Dim() .. badge .. "|r")
                iRow.badgeText:Show()
            end

            -- Remove button (decrements qty, removes at 0)
            iRow.removeBtn:Show()
            local entryItemID = entry.itemID
            iRow.removeBtn:SetScript("OnClick", function()
                if HDG.Store then
                    HDG.Store:RemoveFromVendorShoppingList(entryItemID, nil)
                    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

    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(HDG.Theme.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  " .. HDG.Theme.C_Dim() .. #currentZoneVendors .. " vendors, " .. totalItems .. " uncollected|r")
    if scheme then ApplyFont(zRow.nameText, scheme, "body", 13) 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 HDG.Theme.C_Dim() .. "v|r" or HDG.Theme.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(HDG.Theme.C_Accent() .. vendor.name .. "|r")
        if scheme then ApplyFont(vRow.nameText, scheme, "body", 13) end

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

        -- Coords
        if vendor.x and vendor.x > 0 then
            vRow.rightText:SetText(HDG.Theme.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(HDG.Theme.C_Success() .. "[HDG]|r Added " .. HDG.Theme.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.78, 0.78, 0.78)
            GameTooltip:AddLine("Shift-Click to add all uncollected items", 0.78, 0.78, 0.78)
            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

                -- 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(HDG.Theme.C_Success() .. "[HDG]|r Added " .. HDG.Theme.C_Accent() .. name .. "|r to shopping list")
                        end
                    end
                end)
                local byItemVe = HDG_VendorDB and HDG_VendorDB[vNpcID]
                local byItemIE = byItemVe and byItemVe.items and byItemVe.items[uiItemID]
                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)
                            if byItemIE then
                                local gc = byItemIE[1] or 0
                                if gc > 0 then
                                    GameTooltip:AddLine("Cost: " .. GetCoinTextureString(gc), 1, 1, 1)
                                end
                                if byItemIE[5] then
                                    for _, ct in ipairs(byItemIE[5]) do
                                        if ct[1] == "c" then
                                            local info = C_CurrencyInfo and C_CurrencyInfo.GetCurrencyInfo(ct[2])
                                            local cName = info and info.name or ("Currency " .. ct[2])
                                            local icon = info and info.iconFileID
                                            local iconStr = icon and string.format("|T%s:14:14|t", icon) or ""
                                            GameTooltip:AddLine(iconStr .. " " .. ct[3] .. " " .. cName, 0.7, 0.8, 1.0)
                                        end
                                    end
                                end
                            end
                            GameTooltip:AddLine(" ")
                            GameTooltip:AddLine("Shift-Click to add to Shopping List", 0.78, 0.78, 0.78)
                            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

-- ============================================================================
-- STYLE MODE
-- ============================================================================

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

    local list = HDG.Store:GetState().styleShoppingList
    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(HDG.Theme.C_Dim() .. "No style items tracked.\nUse the Styles tab to add items.|r")
        row.nameText:SetWordWrap(true)
        scrollContent:SetHeight(40)
        return
    end

    -- Group items by styleId
    local styleGroups = {} -- [styleId] = { items = {entry,...} }
    local styleOrder = {}
    for _, entry in ipairs(list) do
        local sid = entry.styleId
        if not styleGroups[sid] then
            styleGroups[sid] = { items = {} }
            table.insert(styleOrder, sid)
        end
        table.insert(styleGroups[sid].items, entry)
    end

    -- Sort groups alphabetically by style display name
    local StyleEngine = privateDB and privateDB.HDG_StyleEngine
    local function GetStyleName(styleId)
        if StyleEngine and StyleEngine.GetAllStyleDefs then
            local defs = StyleEngine:GetAllStyleDefs()
            if defs and defs[styleId] then return defs[styleId].displayName end
        end
        return styleId
    end
    table.sort(styleOrder, function(a, b) return (GetStyleName(a) or "") < (GetStyleName(b) or "") end)

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

    for _, styleId in ipairs(styleOrder) do
        local group = styleGroups[styleId]
        local styleName = GetStyleName(styleId)
        local isExpanded = expandedStyles[styleId]

        -- Count collected
        local collected = 0
        for _, entry in ipairs(group.items) do
            if HDG_Data and HDG_Data.IsItemCollected(entry.itemID) then
                collected = collected + 1
            end
        end

        -- Style header row
        local sRow = GetPooledRow(scrollContent, FW.VENDOR_LIST_ZONE_HEADER_HEIGHT)
        sRow:SetPoint("TOPLEFT", 0, -yOffset)
        sRow:SetPoint("RIGHT", scrollContent, "RIGHT", 0, 0)
        if scheme then
            sRow:SetBackdropColor(scheme.accent.r, scheme.accent.g, scheme.accent.b, 0.15)
            sRow:SetBackdropBorderColor(scheme.accent.r, scheme.accent.g, scheme.accent.b, 0.3)
        end

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

        sRow.nameText:ClearAllPoints()
        sRow.nameText:SetPoint("LEFT", 14, 0)
        sRow.nameText:SetWidth(contentWidth - 80)
        sRow.nameText:SetText("|cFFe2e8f0" .. styleName .. "|r")
        if scheme then ApplyFont(sRow.nameText, scheme, "body", 13) end

        -- Collected/total count
        sRow.rightText:SetText(HDG.Theme.C_Dim() .. collected .. "/" .. #group.items .. "|r")
        if scheme then ApplyFont(sRow.rightText, scheme, "small") end

        -- Click to expand/collapse
        local sStyleId = styleId
        sRow:SetScript("OnClick", function()
            expandedStyles[sStyleId] = not expandedStyles[sStyleId]
            HDG_VendorShoppingList.Refresh()
        end)
        sRow: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
        end)
        sRow:SetScript("OnLeave", function(self)
            local c = GetScheme()
            if c then self:SetBackdropColor(c.accent.r, c.accent.g, c.accent.b, 0.15) end
        end)
        yOffset = yOffset + FW.VENDOR_LIST_ZONE_HEADER_HEIGHT

        -- Item rows (only if expanded)
        if isExpanded then
            -- Sort items alphabetically
            table.sort(group.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(group.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)

                local isCollected = HDG_Data and HDG_Data.IsItemCollected(entry.itemID)
                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 (green if collected, qty prefix if >1)
                local itemName = HDG_Data and HDG_Data.GetCachedItemName(entry.itemID) or ("Item " .. entry.itemID)
                local qty = entry.qty or 1
                local qtyPrefix = qty > 1 and (HDG.Theme.C_Accent() .. qty .. "x|r ") or ""
                iRow.nameText:ClearAllPoints()
                iRow.nameText:SetPoint("LEFT", iRow.iconTex, "RIGHT", 4, 0)
                iRow.nameText:SetWidth(contentWidth - FW.VENDOR_LIST_INDENT_ITEM - 70)
                if isCollected then
                    iRow.nameText:SetText(qtyPrefix .. HDG.Theme.C_Success() .. itemName .. "|r")
                else
                    iRow.nameText:SetText(qtyPrefix .. itemName)
                end
                if scheme then ApplyFont(iRow.nameText, scheme, "small") end

                -- Shift-click: search AH for craftable items
                if entry.sourceType == 6 then
                    local eItemName = itemName
                    iRow:SetScript("OnClick", function()
                        if IsShiftKeyDown() then SearchAuctionHouse(eItemName) end
                    end)
                end

                -- Source badge
                local badge = GetSourceBadge(entry.sourceType)
                if badge ~= "" then
                    iRow.badgeText:ClearAllPoints()
                    iRow.badgeText:SetPoint("LEFT", iRow.nameText, "RIGHT", 2, 0)
                    iRow.badgeText:SetText(HDG.Theme.C_Dim() .. badge .. "|r")
                    iRow.badgeText:Show()
                end

                -- Remove button
                iRow.removeBtn:Show()
                local entryItemID, entryStyleId = entry.itemID, entry.styleId
                iRow.removeBtn:SetScript("OnClick", function()
                    if HDG.Store then
                        HDG.Store:RemoveFromStyleShoppingList(entryItemID, entryStyleId)
                        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

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

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

function HDG_VendorShoppingList.MapListWaypoints()
    if not HDG.Store or not HDG.Waypoints then return end
    local list = HDG.Store:GetState().vendorShoppingList
    local seen = {}
    local waypoints = {}
    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)
                waypoints[#waypoints + 1] = { ve[3], ve[4], ve[5], name }
            end
        end
    end
    if #waypoints == 0 then
        print("|cFFFFAA00[HDG]|r No valid vendor locations to map.")
        return
    end
    local count, provider = HDG.Waypoints:SetMultiple(waypoints)
    HDG.Waypoints:PrintResult(count > 0, provider, nil, count, waypoints[1] and waypoints[1][1])
end

function HDG_VendorShoppingList.MapZoneWaypoints()
    if not currentZoneVendors or not HDG.Waypoints then return end
    local waypoints = {}
    for _, vendor in ipairs(currentZoneVendors) do
        waypoints[#waypoints + 1] = { vendor.mapID, vendor.x, vendor.y, vendor.name }
    end
    if #waypoints == 0 then
        print("|cFFFFAA00[HDG]|r No valid vendor locations to map.")
        return
    end
    local count, provider = HDG.Waypoints:SetMultiple(waypoints)
    HDG.Waypoints:PrintResult(count > 0, provider, nil, count, waypoints[1] and waypoints[1][1])
end

-- ============================================================================
-- EXPORT / IMPORT
-- ============================================================================

-- Format: "HDGSL:1:<base64>" where payload is newline-separated entries
-- Vendor entries: "V,<itemID>,<npcID>"
-- Style entries:  "S,<itemID>,<styleId>,<qty>"

local b64chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
local b64enc, b64dec = {}, {}
for i = 1, 64 do
    local c = b64chars:sub(i, i)
    b64enc[i - 1] = c
    b64dec[c:byte()] = i - 1
end

local function B64Encode(data)
    local out = {}
    local len = #data
    for i = 1, len, 3 do
        local a = data:byte(i)
        local b = i + 1 <= len and data:byte(i + 1) or 0
        local c = i + 2 <= len and data:byte(i + 2) or 0
        out[#out + 1] = b64enc[bit.rshift(a, 2)]
        out[#out + 1] = b64enc[bit.bor(bit.lshift(bit.band(a, 3), 4), bit.rshift(b, 4))]
        out[#out + 1] = (i + 1 <= len) and b64enc[bit.bor(bit.lshift(bit.band(b, 15), 2), bit.rshift(c, 6))] or "="
        out[#out + 1] = (i + 2 <= len) and b64enc[bit.band(c, 63)] or "="
    end
    return table.concat(out)
end

local function B64Decode(data)
    local out = {}
    data = data:gsub("[^A-Za-z0-9+/=]", "")
    for i = 1, #data, 4 do
        local a = b64dec[data:byte(i)] or 0
        local b = b64dec[data:byte(i + 1)] or 0
        local c = b64dec[data:byte(i + 2)] or 0
        local d = b64dec[data:byte(i + 3)] or 0
        out[#out + 1] = string.char(bit.bor(bit.lshift(a, 2), bit.rshift(b, 4)))
        if data:sub(i + 2, i + 2) ~= "=" then
            out[#out + 1] = string.char(bit.band(bit.bor(bit.lshift(b, 4), bit.rshift(c, 2)), 0xFF))
        end
        if data:sub(i + 3, i + 3) ~= "=" then
            out[#out + 1] = string.char(bit.band(bit.bor(bit.lshift(c, 6), d), 0xFF))
        end
    end
    return table.concat(out)
end

local eiDialog -- export/import dialog frame (created once)

local function SerializeVendorList()
    if not HDG.Store then return "" end
    local lines = {}
    for _, entry in ipairs(HDG.Store:GetState().vendorShoppingList) do
        table.insert(lines, entry.itemID .. "," .. entry.npcID)
    end
    return "HDGVL:1:" .. B64Encode(table.concat(lines, "\n"))
end

local function SerializeStyleList()
    if not HDG.Store then return "" end
    local lines = {}
    for _, entry in ipairs(HDG.Store:GetState().styleShoppingList) do
        table.insert(lines, entry.itemID .. "," .. entry.styleId .. "," .. (entry.qty or 1))
    end
    return "HDGSL:1:" .. B64Encode(table.concat(lines, "\n"))
end

local function DeserializeVendorList(encoded)
    if not encoded or encoded == "" then return nil, "Empty string" end
    local prefix, ver, payload = encoded:match("^(HDGVL):(%d+):(.+)$")
    if not prefix then return nil, "Invalid format (expected HDGVL:1:...)" end
    if ver ~= "1" then return nil, "Unknown version: " .. ver end
    local decoded = B64Decode(payload)
    if not decoded or decoded == "" then return nil, "Failed to decode payload" end
    local entries = {}
    for line in decoded:gmatch("[^\n]+") do
        local itemID, npcID = strsplit(",", line)
        itemID, npcID = tonumber(itemID), tonumber(npcID)
        if itemID and npcID then
            table.insert(entries, { itemID = itemID, npcID = npcID })
        end
    end
    return entries
end

local function DeserializeStyleList(encoded)
    if not encoded or encoded == "" then return nil, "Empty string" end
    local prefix, ver, payload = encoded:match("^(HDGSL):(%d+):(.+)$")
    if not prefix then return nil, "Invalid format (expected HDGSL:1:...)" end
    if ver ~= "1" then return nil, "Unknown version: " .. ver end
    local decoded = B64Decode(payload)
    if not decoded or decoded == "" then return nil, "Failed to decode payload" end
    local entries = {}
    for line in decoded:gmatch("[^\n]+") do
        local itemID, styleId, qty = strsplit(",", line)
        itemID = tonumber(itemID)
        qty = tonumber(qty) or 1
        if itemID and styleId and styleId ~= "" then
            table.insert(entries, { itemID = itemID, styleId = styleId, qty = qty })
        end
    end
    return entries
end

local function CreateExportImportDialog()
    if eiDialog then return end

    local f = CreateFrame("Frame", "HDG_ShoppingListEIFrame", UIParent, "BackdropTemplate")
    f:SetSize(420, 260)
    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 scheme = GetScheme()
    if scheme then
        f:SetBackdropColor(scheme.panel.r, scheme.panel.g, scheme.panel.b, 0.95)
        f:SetBackdropBorderColor(scheme.accent.r, scheme.accent.g, scheme.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)
    f:Hide()

    -- Title
    f.title = f:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    f.title:SetPoint("TOP", 0, -8)
    if scheme then ApplyFont(f.title, scheme, "body") end

    -- Close button
    local closeBtn = CreateFrame("Button", nil, f, "UIPanelCloseButton")
    closeBtn:SetPoint("TOPRIGHT", -2, -2)
    closeBtn:SetSize(20, 20)

    -- Scroll + EditBox
    local sf = CreateFrame("ScrollFrame", nil, f, "UIPanelScrollFrameTemplate")
    sf:SetPoint("TOPLEFT", 8, -30)
    sf:SetPoint("BOTTOMRIGHT", -28, 40)

    local eb = CreateFrame("EditBox", nil, sf)
    eb:SetMultiLine(true)
    eb:SetAutoFocus(false)
    eb:SetFontObject(GameFontHighlightSmall)
    eb:SetWidth(sf:GetWidth() or 380)
    eb:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
    sf:SetScrollChild(eb)
    f.editBox = eb

    -- Action button (Export: copy / Import: apply)
    f.actionBtn = CreateFrame("Button", nil, f, "UIPanelButtonTemplate")
    f.actionBtn:SetSize(120, 24)
    f.actionBtn:SetPoint("BOTTOMRIGHT", -8, 8)

    -- Status text
    f.status = f:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    f.status:SetPoint("BOTTOMLEFT", 8, 14)
    f.status:SetJustifyH("LEFT")

    table.insert(UISpecialFrames, "HDG_ShoppingListEIFrame")
    eiDialog = f
end

function HDG_VendorShoppingList.ShowExportImport(mode, listType)
    CreateExportImportDialog()
    local eb = eiDialog.editBox
    eiDialog.status:SetText("")

    local isVendor = (listType == "vendor")
    local label = isVendor and "Vendor" or "Style"
    local prefix = isVendor and "HDGVL" or "HDGSL"

    if mode == "export" then
        eiDialog.title:SetText("|cFFe2e8f0Export " .. label .. " List|r")
        local encoded = isVendor and SerializeVendorList() or SerializeStyleList()
        eb:SetText(encoded)
        eb:HighlightText()
        eb:SetFocus()
        eiDialog.actionBtn:SetText("Select All")
        eiDialog.actionBtn:SetScript("OnClick", function()
            eb:HighlightText()
            eb:SetFocus()
        end)
        local state = HDG.Store:GetState()
        local count = isVendor and #state.vendorShoppingList or #state.styleShoppingList
        eiDialog.status:SetText(HDG.Theme.C_Dim() .. count .. " entries|r")
    else
        eiDialog.title:SetText("|cFFe2e8f0Import " .. label .. " List|r")
        eb:SetText("")
        eb:SetFocus()
        eiDialog.actionBtn:SetText("Import")
        eiDialog.actionBtn:SetScript("OnClick", function()
            local text = eb:GetText():match("^%s*(.-)%s*$") -- trim
            if not text or text == "" then
                eiDialog.status:SetText("|cFFFF4444Paste an export string first.|r")
                return
            end
            if isVendor then
                local entries, err = DeserializeVendorList(text)
                if not entries then
                    eiDialog.status:SetText("|cFFFF4444" .. (err or "Invalid data") .. "|r")
                    return
                end
                local added = 0
                for _, ve in ipairs(entries) do
                    if HDG.Store:AddToVendorShoppingList(ve.itemID, ve.npcID) then
                        added = added + 1
                    end
                end
                eiDialog.status:SetText(HDG.Theme.C_Success() .. "Imported " .. added .. " vendor entries|r")
            else
                local entries, err = DeserializeStyleList(text)
                if not entries then
                    eiDialog.status:SetText("|cFFFF4444" .. (err or "Invalid data") .. "|r")
                    return
                end
                local added = 0
                for _, se in ipairs(entries) do
                    for _ = 1, se.qty do
                        HDG.Store:AddToStyleShoppingList(se.itemID, se.styleId)
                    end
                    added = added + 1
                end
                eiDialog.status:SetText(HDG.Theme.C_Success() .. "Imported " .. added .. " style entries|r")
            end
            HDG_VendorShoppingList.Refresh()
        end)
        eiDialog.status:SetText(HDG.Theme.C_Dim() .. "Paste a " .. prefix .. ":1:... string and click Import|r")
    end

    eiDialog:Show()
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() + HDG.Store:GetStyleShoppingListCount()
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 isStyleMode = activeMode == "style"
    local isConfigMode = activeMode == "config"

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

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

    -- Refresh config checkboxes
    if isConfigMode and widget.cbZoneScanner then
        widget.cbZoneScanner:SetChecked(GetConfig("zoneScannerEnabled") or false)
        widget.cbZonePopup:SetChecked(GetConfig("zoneScannerPopup") or false)
        widget.cbZoneSquirrels:SetChecked(GetConfig("zoneScannerSquirrels") or false)
        widget.cbZoneChat:SetChecked(GetConfig("zoneScannerChat") ~= false)
        widget.cbZoneSound:SetChecked(GetConfig("zoneScannerSound") or false)
        if widget._updateScannerSubOptions then widget._updateScannerSubOptions() end
    end

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

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

    if not GetConfig("zoneScannerEnabled") or InCombatLockdown() then
        if HDG_VendorShoppingList.IsShown() and activeMode == "zone" then
            HDG_VendorShoppingList.Refresh()
        end
        return
    end

    local wantPopup = GetConfig("zoneScannerPopup") or false
    local wantSquirrels = GetConfig("zoneScannerSquirrels") or false
    local wantChat = GetConfig("zoneScannerChat") ~= false
    local wantSound = GetConfig("zoneScannerSound") or false
    local didAlert = false

    -- Check: zone has uncollected vendor items
    if HDG_VendorResolver.ZoneHasUncollectedVendorItems(mapID) then
        if wantPopup then HDG_VendorShoppingList.Show("zone") end
        if wantSquirrels then
            local quote, speaker = privateDB.HDG_CraftQuotes.GetRandomZoneAlertQuote()
            privateDB.HDG_Vamoose.DisplayQuote(quote, speaker)
            if wantChat then print("|cFFA0522D[Squirrel]|r " .. quote) end
            didAlert = true
        elseif wantChat then
            local quote = privateDB.HDG_CraftQuotes.GetRandomZoneAlertQuote()
            print("|cFFA0522D[Squirrel]|r " .. quote)
            didAlert = true
        end
        if wantSound then PlaySound(SOUNDKIT.IG_MAINMENU_OPEN) end
    end

    -- Check: shopping/style list items have vendors in this zone
    if mapID then
        local state = HDG.Store:GetState()
        local found = false
        for _, entry in ipairs(state.vendorShoppingList) do
            local ve = HDG_VendorDB and HDG_VendorDB[entry.npcID]
            if ve and ve[3] == mapID then found = true; break end
        end
        if not found and HDG_VendorByItem then
            for _, entry in ipairs(state.styleShoppingList) do
                local npcIDs = HDG_VendorByItem[entry.itemID]
                if npcIDs then
                    for _, npcID in ipairs(npcIDs) do
                        local ve = HDG_VendorDB and HDG_VendorDB[npcID]
                        if ve and ve[3] == mapID then found = true; break end
                    end
                    if found then break end
                end
            end
        end
        if found then
            if wantPopup and not HDG_VendorShoppingList.IsShown() then
                HDG_VendorShoppingList.Show("zone")
            end
            if wantSquirrels then
                local quote, speaker = privateDB.HDG_CraftQuotes.GetRandomShoppingZoneQuote()
                if not didAlert then privateDB.HDG_Vamoose.DisplayQuote(quote, speaker) end
                if wantChat then print("|cFFA0522D[Squirrel]|r " .. quote) end
            elseif wantChat then
                local quote = privateDB.HDG_CraftQuotes.GetRandomShoppingZoneQuote()
                print("|cFFA0522D[Squirrel]|r " .. quote)
            end
            if wantSound and not didAlert then PlaySound(SOUNDKIT.IG_MAINMENU_OPEN) end
        end
    end

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