local PT = AhUI
if not PT then return end

PT.ShoppingList = PT.ShoppingList or {}
local S = PT.ShoppingList

local P = S._Private or {}
local FONT = S.FONT or AhUI.FONT or STANDARD_TEXT_FONT
local LibAHTab = S.LibAHTab or (LibStub and LibStub("LibAHTab-1-0", true))
local function GetLibAHTab()
    if not LibAHTab and LibStub then
        LibAHTab = LibStub("LibAHTab-1-0", true)
        S.LibAHTab = LibAHTab
    end
    return LibAHTab
end

local AH_TAB_ID = S.AH_TAB_ID or "AhUIProfessionShopping"
local AH_TAB_TEXT_KEY = S.AH_TAB_TEXT_KEY or "PG_SHOP_TAB"
local TITLE = S.TITLE or "PG_SHOP_TITLE"
local ROW_HEIGHT = S.ROW_HEIGHT or 28
local ROW_GAP = S.ROW_GAP or 2

local PANEL_PAD = 12
local SIDEBAR_W = 176
local TOP_H = 36
local HEADER_H = 22
local ACTION_W = 28
local STATUS_W = 72
local AH_W = 48
local BUY_BTN_SIZE = 24
local BUY_ICON_SIZE = 24
local BUY_CONFIRM_ICON_SIZE = 24
local PRICE_W = 104
local CHOSEN_W = 48
local QTY_W = 40
local NAME_TIER_ICON_SIZE = 16
local CHOSEN_TIER_ICON_SIZE = 16
local SEARCH_BTN_GAP = 4
local SIDEBAR_TAB_W = 76
local SIDEBAR_TAB_H = 24
local SIDEBAR_TAB_GAP = 2

S.shoppingSidebarMode = S.shoppingSidebarMode or "lists"

local function T(key, default)
    if P.T then
        return P.T(key, default)
    end
    local L = PT.L or {}
    return L[key] or default or key
end

local function FormatMoneyText(value)
    if P.FormatMoneyText then return P.FormatMoneyText(value) end
    if type(value) ~= "number" or value <= 0 then return T("PG_DASH", "—") end
    return GetCoinTextureString(value, 12) or tostring(value)
end

local function GetItemIcon(itemID)
    if P.GetItemIcon then return P.GetItemIcon(itemID) end
    local _, _, _, _, icon = GetItemInfoInstant(itemID)
    return icon or 134400
end

local function RequestSessionItemData()
    if P.RequestSessionItemData then return P.RequestSessionItemData() end
end

local function GetRemainingQuantity(row)
    if P.GetRemainingQuantity then return P.GetRemainingQuantity(row) end
    return math.max(0, tonumber(row and row.remainingQuantity) or tonumber(row and row.quantity) or 0)
end

local function GetVisibleRows()
    if P.GetVisibleRows then return P.GetVisibleRows() end
    local out = {}
    for _, row in ipairs(S.session and S.session.rows or {}) do
        if not row.hidden then table.insert(out, row) end
    end
    return out
end

local function HasShoppingSession()
    if P.HasShoppingSession then return P.HasShoppingSession() end
    return S.session and S.session.active and S.session.rows ~= nil
end

local function ShouldShowAuctionTab()
    if P.ShouldShowAuctionTab then return P.ShouldShowAuctionTab() end
    return HasShoppingSession() or (PT.config and PT.config.shoppingListAlwaysShowTab == true)
end

local function UpdateAuctionTabVisibility()
    if P.UpdateAuctionTabVisibility then
        P.UpdateAuctionTabVisibility()
        return
    end

    local tabLib = GetLibAHTab()
    if not tabLib or not tabLib.DoesIDExist or not tabLib.GetButton then return end
    if not tabLib:DoesIDExist(AH_TAB_ID) then return end

    local button = tabLib:GetButton(AH_TAB_ID)
    if button then
        button:SetShown(ShouldShowAuctionTab())
    end
end

local function ShowRowTooltip(widget)
    if P.ShowRowTooltip then return P.ShowRowTooltip(widget) end
end

local function AnchorTooltipAtCursor()
    if not GameTooltip or not UIParent or not GetCursorPosition then return end
    local scale = UIParent:GetEffectiveScale() or 1
    local x, y = GetCursorPosition()
    GameTooltip:ClearAllPoints()
    GameTooltip:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", x / scale, y / scale)
end

local function ShowItemTooltip(widget, itemID)
    if not widget or not itemID or not GameTooltip then return end
    GameTooltip:SetOwner(widget, "ANCHOR_NONE")
    GameTooltip:SetHyperlink("item:" .. itemID)
    AnchorTooltipAtCursor()
    GameTooltip:Show()
end

local function GetRowBaseLabel(row)
    if P.GetRowBaseLabel then return P.GetRowBaseLabel(row) end
    return row and row.label or "?"
end

local function TrimText(value)
    value = tostring(value or "")
    value = value:gsub("^%s+", "")
    value = value:gsub("%s+$", "")
    return value
end

local function Lower(value)
    return string.lower(tostring(value or ""))
end

local function SetFrameBackdrop(frame, alpha)
    if not frame then return end
    frame:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8x8",
        edgeFile = "Interface\\Buttons\\WHITE8x8",
        edgeSize = 1,
    })
    frame:SetBackdropColor(0.05, 0.05, 0.07, alpha or 0.92)
    frame:SetBackdropBorderColor(1, 1, 1, 0.10)
end

local function SetFrameAtlasBackground(frame, atlas, alpha)
    if not frame or not atlas then return end

    if not frame.ahuiAtlasBG then
        frame.ahuiAtlasBG = frame:CreateTexture(nil, "BACKGROUND", nil, -1)
        frame.ahuiAtlasBG:SetPoint("TOPLEFT", 1, -1)
        frame.ahuiAtlasBG:SetPoint("BOTTOMRIGHT", -1, 1)
    end

    frame.ahuiAtlasBG:SetAtlas(atlas)
    frame.ahuiAtlasBG:SetAlpha(alpha or 0.18)
    frame.ahuiAtlasBG:Show()
end

local function MakeText(parent, size, justify)
    local fs = parent:CreateFontString(nil, "OVERLAY")
    fs:SetFont(FONT, size or 11, "")
    fs:SetJustifyH(justify or "LEFT")
    fs:SetJustifyV("MIDDLE")
    return fs
end

local function ShowSimpleTooltip(owner, title, body)
    if not owner or not GameTooltip then return end
    GameTooltip:SetOwner(owner, "ANCHOR_NONE")
    GameTooltip:SetText(title or "")
    if body and body ~= "" then
        GameTooltip:AddLine(body, 0.8, 0.8, 0.8, true)
    end
    AnchorTooltipAtCursor()
    GameTooltip:Show()
end

local function CreateCheckLine(parent, text, tooltipTitle, tooltipText, getChecked, setChecked)
    local holder = CreateFrame("Frame", nil, parent)
    holder:SetSize(150, 20)
    holder:EnableMouse(true)

    holder.check = CreateFrame("CheckButton", nil, holder, "UICheckButtonTemplate")
    holder.check:SetSize(22, 22)
    holder.check:SetPoint("LEFT", -2, 0)

    holder.text = MakeText(holder, 10, "LEFT")
    holder.text:SetPoint("LEFT", holder.check, "RIGHT", -2, 0)
    holder.text:SetPoint("RIGHT", holder, "RIGHT", 0, 0)
    holder.text:SetText(text)

    function holder:Refresh()
        self.check:SetChecked(getChecked and getChecked() == true)
    end

    local function Toggle()
        local value = not (getChecked and getChecked() == true)
        if setChecked then
            setChecked(value)
        end
        holder:Refresh()
    end

    holder.check:SetScript("OnClick", function(self)
        if setChecked then
            setChecked(self:GetChecked() == true)
        end
        holder:Refresh()
    end)

    holder:SetScript("OnMouseUp", function(_, button)
        if button == "LeftButton" then
            Toggle()
        end
    end)

    local function OnEnter(self)
        ShowSimpleTooltip(self, tooltipTitle, tooltipText)
    end

    local function OnLeave()
        GameTooltip:Hide()
    end

    holder:SetScript("OnEnter", OnEnter)
    holder:SetScript("OnLeave", OnLeave)
    holder.check:SetScript("OnEnter", OnEnter)
    holder.check:SetScript("OnLeave", OnLeave)

    holder:Refresh()

    return holder
end

local function SetButtonAtlas(button, atlas)
    if not button then return end

    if not button.icon then
        button.icon = button:CreateTexture(nil, "ARTWORK")
        button.icon:SetPoint("CENTER", 0, 0)
    end

    local size = button.iconSize or 18
    button.icon:SetSize(size, size)

    local ok = false
    if atlas and button.icon.SetAtlas then
        ok = pcall(button.icon.SetAtlas, button.icon, atlas)
    end

    if not ok then
        button.icon:SetTexture("Interface\\Buttons\\UI-RefreshButton")
    end
end

local function IsChoiceRow(rowData)
    return rowData and type(rowData.options) == "table" and #rowData.options > 0
end

local function IsQuantityEditable(rowData)
    if not rowData then
        return false
    end

    if rowData.purchaseStage == "await_price" or rowData.purchaseStage == "processing" then
        return false
    end

    if rowData.isSearchResult then
        return rowData.isCommodity == true
    end

    if rowData.isCommodity == false then
        return false
    end

    return true
end

local function GetDisplayItemID(rowData)
    if not rowData then
        return nil
    end

    return rowData.chosenItemID
        or rowData.itemID
        or (rowData.options and rowData.options[1] and rowData.options[1].itemID)
end

local function GetDisplayItemQuality(rowData)
    if not rowData then
        return nil
    end

    local quality = tonumber(rowData.quality or rowData.itemQuality)
    if quality ~= nil then
        return quality
    end

    local itemID = GetDisplayItemID(rowData)
    if itemID then
        local _, _, itemQuality = GetItemInfo(itemID)
        return tonumber(itemQuality)
    end

    return nil
end

local ITEM_CLASS_WEAPON = Enum and Enum.ItemClass and Enum.ItemClass.Weapon or 2
local ITEM_CLASS_ARMOR = Enum and Enum.ItemClass and Enum.ItemClass.Armor or 4

local function IsArmorOrWeapon(rowData)
    if not rowData then
        return false
    end

    local classID = tonumber(rowData.itemClassID or rowData.classID)
    if not classID then
        local itemID = GetDisplayItemID(rowData)
        if itemID then
            local _, _, _, _, _, _, _, _, _, _, _, readClassID = GetItemInfo(itemID)
            classID = tonumber(readClassID)
        end
    end

    return classID == ITEM_CLASS_WEAPON or classID == ITEM_CLASS_ARMOR
end

local function GetDisplayItemLevel(rowData)
    if not rowData then
        return nil
    end

    local itemLevel = tonumber(rowData.itemLevel or rowData.ilvl)
    if itemLevel and itemLevel > 0 then
        return math.floor(itemLevel)
    end

    local itemID = GetDisplayItemID(rowData)
    if itemID then
        local _, _, _, readItemLevel = GetItemInfo(itemID)
        readItemLevel = tonumber(readItemLevel)
        if readItemLevel and readItemLevel > 0 then
            return math.floor(readItemLevel)
        end
    end

    return nil
end

local function GetRowDisplayName(rowData)
    local name = GetRowBaseLabel(rowData)

    if IsArmorOrWeapon(rowData) then
        local itemLevel = GetDisplayItemLevel(rowData)
        if itemLevel then
            return string.format("%s (%d)", name, itemLevel)
        end
    end

    return name
end

local function ApplyNameColor(row, rowData)
    if not row or not row.name then
        return
    end

    local quality = GetDisplayItemQuality(rowData)
    local color = quality and ITEM_QUALITY_COLORS and ITEM_QUALITY_COLORS[quality] or nil

    if color then
        row.name:SetTextColor(color.r or 1, color.g or 1, color.b or 1)
    else
        row.name:SetTextColor(1, 1, 1)
    end
end

local function GetTierFromRow(rowData)
    if not rowData then
        return nil
    end

    local tier = tonumber(rowData.chosenTier or rowData.professionQuality or rowData.craftedQuality or rowData.materialQuality)
    if not tier and rowData.lastChosenName then
        tier = tonumber(string.match(tostring(rowData.lastChosenName), "R(%d+)"))
    end

    if not tier then
        return nil
    end

    tier = math.floor(tier)
    if tier < 1 or tier > 5 then
        return nil
    end

    return tier
end

local function GetChosenDisplay(rowData)
    local tier = GetTierFromRow(rowData)
    if tier then
        return tostring(tier)
    end

    if rowData and rowData.lastChosenName and rowData.lastChosenName ~= "" and rowData.lastChosenName ~= T("PG_DASH", "—") then
        return rowData.lastChosenName
    end

    if IsChoiceRow(rowData) then
        return "…"
    end

    return T("PG_DASH", "—")
end

local GetProfessionTierAtlas

local function SetChosenDisplay(row, rowData)
    if not row or not row.chosen then
        return
    end

    if not IsChoiceRow(rowData) then
        if row.chosenIcon then
            row.chosenIcon:Hide()
        end
        row.chosen:SetText(T("PG_DASH", "—"))
        return
    end

    local atlas = GetProfessionTierAtlas and GetProfessionTierAtlas(rowData) or nil

    if atlas and row.chosenIcon and row.chosenIcon.SetAtlas then
        local ok = pcall(row.chosenIcon.SetAtlas, row.chosenIcon, atlas)
        if ok then
            row.chosen:SetText("")
            row.chosenIcon:Show()
            return
        end
    end

    if row.chosenIcon then
        row.chosenIcon:Hide()
    end

    row.chosen:SetText(GetChosenDisplay(rowData))
end

local function RowMatchesSearch(rowData, query)
    if not query or query == "" then
        return true
    end

    local name = Lower(GetRowBaseLabel(rowData))
    if string.find(name, query, 1, true) then
        return true
    end

    if rowData and rowData.itemID and string.find(tostring(rowData.itemID), query, 1, true) then
        return true
    end

    for _, opt in ipairs(rowData and rowData.options or {}) do
        if opt.itemID and string.find(tostring(opt.itemID), query, 1, true) then
            return true
        end
    end

    return false
end

local function ApplyQuantityFromBox(box)
    if not box then return end

    local searchRow = box.searchRow
    if searchRow and searchRow.isSearchResult then
        if not IsQuantityEditable(searchRow) then
            box:SetText("1")
            box:ClearFocus()
            return
        end

        local text = TrimText(box:GetText())
        local quantity = tonumber(text)
        if not quantity then
            local current = GetRemainingQuantity(searchRow)
            box:SetText(tostring(current > 0 and current or 1))
            box:ClearFocus()
            return
        end

        quantity = math.floor(quantity)
        if quantity < 1 then quantity = 1 end
        if quantity > 999999 then quantity = 999999 end

        searchRow.quantity = quantity
        searchRow.remainingQuantity = quantity
        box:SetText(tostring(quantity))
        box:ClearFocus()

        if S.RefreshWindow then
            S:RefreshWindow()
        end
        return
    end

    local rowIndex = box.rowIndex
    if not rowIndex then return end

    local text = TrimText(box:GetText())
    local quantity = tonumber(text)
    if not quantity then
        local row = S.session and S.session.rows and S.session.rows[rowIndex]
        box:SetText(tostring(GetRemainingQuantity(row)))
        box:ClearFocus()
        return
    end

    quantity = math.floor(quantity)
    if quantity < 0 then quantity = 0 end

    if S.SetRowQuantity then
        S:SetRowQuantity(rowIndex, quantity)
    else
        local row = S.session and S.session.rows and S.session.rows[rowIndex]
        if row then
            row.quantity = quantity
            row.remainingQuantity = quantity
        end
        if S.RefreshWindow then S:RefreshWindow() end
    end

    box:ClearFocus()
end
local function CreateIconButton(parent, atlas, tooltipTitle, tooltipText, buttonSize, iconSize)
    local btn = CreateFrame("Button", nil, parent)
    btn:SetSize(buttonSize or 24, buttonSize or 24)
    btn:SetHighlightTexture("Interface\\Buttons\\ButtonHilight-Square", "ADD")

    btn.iconSize = iconSize or 18
    SetButtonAtlas(btn, atlas)

    btn.tooltipTitle = tooltipTitle
    btn.tooltipText = tooltipText

    btn:SetScript("OnEnter", function(self)
        ShowSimpleTooltip(self, self.tooltipTitle, self.tooltipText)
    end)

    btn:SetScript("OnLeave", function()
        GameTooltip:Hide()
    end)

    return btn
end

local promptPayloads = {}

local function GetPopupEditBox(popup)
    if not popup then
        return nil
    end

    if popup.editBox then
        return popup.editBox
    end

    if popup.EditBox then
        return popup.EditBox
    end

    local name = popup.GetName and popup:GetName() or nil
    if name then
        return _G[name .. "EditBox"] or _G[name .. "EditBox1"]
    end

    return nil
end

local function PromptText(dialogKey, title, defaultValue, onAccept)
    defaultValue = tostring(defaultValue or "")

    if not StaticPopupDialogs or not StaticPopup_Show then
        if onAccept then
            onAccept(defaultValue)
        end
        return
    end

    promptPayloads[dialogKey] = {
        defaultValue = defaultValue,
        onAccept = onAccept,
    }

    StaticPopupDialogs[dialogKey] = StaticPopupDialogs[dialogKey] or {
        text = "%s",
        button1 = ACCEPT or "OK",
        button2 = CANCEL or "Cancel",
        hasEditBox = true,
        timeout = 0,
        whileDead = true,
        hideOnEscape = true,
        OnAccept = function(self)
            local payload = promptPayloads[dialogKey]
            local box = GetPopupEditBox(self)
            local value = box and box:GetText() or ""
            if payload and payload.onAccept then
                payload.onAccept(TrimText(value))
            end
            promptPayloads[dialogKey] = nil
        end,
        OnCancel = function()
            promptPayloads[dialogKey] = nil
        end,
        OnShow = function(self)
            local payload = promptPayloads[dialogKey]
            local box = GetPopupEditBox(self)
            if box then
                box:SetText(payload and payload.defaultValue or "")
                box:SetFocus()
                box:HighlightText()
            end
        end,
        EditBoxOnEnterPressed = function(self)
            local parent = self:GetParent()
            if parent and parent.button1 then
                parent.button1:Click()
            elseif parent and StaticPopup_OnClick then
                StaticPopup_OnClick(parent, 1)
            end
        end,
        EditBoxOnEscapePressed = function(self)
            local parent = self:GetParent()
            if parent then
                parent:Hide()
            end
        end,
    }

    local popup = StaticPopup_Show(dialogKey, title or "")
    local box = GetPopupEditBox(popup)
    if box then
        box:SetText(defaultValue)
        box:SetFocus()
        box:HighlightText()
    end
end

local function EnsureImportExportDialog(owner)
    if not owner then
        return nil
    end

    if owner.importExportDialog then
        return owner.importExportDialog
    end

    local d = CreateFrame("Frame", nil, owner, "BackdropTemplate")
    d:SetSize(540, 360)
    d:SetPoint("CENTER", owner, "CENTER", 0, 0)
    d:SetFrameStrata("DIALOG")
    d:SetFrameLevel(80)
    d:EnableMouse(true)
    d:SetMovable(true)
    d:RegisterForDrag("LeftButton")
    d:SetScript("OnDragStart", function(self) self:StartMoving() end)
    d:SetScript("OnDragStop", function(self) self:StopMovingOrSizing() end)
    d:Hide()
    SetFrameBackdrop(d, 0.98)

    d.title = MakeText(d, 13, "LEFT")
    d.title:SetPoint("TOPLEFT", 14, -12)
    d.title:SetPoint("RIGHT", -40, 0)

    d.close = CreateIconButton(d, "common-icon-redx", T("PG_SHOP_CLOSE", "Close"), "", 22, 14)
    d.close:SetPoint("TOPRIGHT", -10, -8)
    d.close:SetScript("OnClick", function()
        d:Hide()
    end)

    d.help = MakeText(d, 10, "LEFT")
    d.help:SetPoint("TOPLEFT", 14, -34)
    d.help:SetPoint("RIGHT", -14, 0)

    d.scroll = CreateFrame("ScrollFrame", nil, d, "UIPanelScrollFrameTemplate")
    d.scroll:SetPoint("TOPLEFT", 14, -58)
    d.scroll:SetPoint("BOTTOMRIGHT", -32, 54)

    d.editBox = CreateFrame("EditBox", nil, d.scroll)
    d.editBox:SetMultiLine(true)
    d.editBox:SetAutoFocus(false)
    d.editBox:SetFont(FONT, 11, "")
    d.editBox:SetWidth(480)
    d.editBox:SetHeight(240)
    d.editBox:SetJustifyH("LEFT")
    d.editBox:SetJustifyV("TOP")
    d.editBox:EnableMouse(true)
    d.editBox:SetScript("OnEscapePressed", function(self)
        self:ClearFocus()
        d:Hide()
    end)
    d.scroll:SetScrollChild(d.editBox)

    d.status = MakeText(d, 10, "LEFT")
    d.status:SetPoint("BOTTOMLEFT", 14, 32)
    d.status:SetPoint("RIGHT", -14, 0)
    d.status:SetText("")

    d.ahui = CreateFrame("Button", nil, d, "UIPanelButtonTemplate")
    d.ahui:SetSize(92, 22)
    d.ahui:SetPoint("BOTTOMLEFT", 14, 8)
    d.ahui:SetText("AhUI")

    d.auctionator = CreateFrame("Button", nil, d, "UIPanelButtonTemplate")
    d.auctionator:SetSize(104, 22)
    d.auctionator:SetPoint("LEFT", d.ahui, "RIGHT", 6, 0)
    d.auctionator:SetText("Auctionator")

    d.importNew = CreateFrame("Button", nil, d, "UIPanelButtonTemplate")
    d.importNew:SetSize(116, 22)
    d.importNew:SetPoint("BOTTOMLEFT", 14, 8)
    d.importNew:SetText(T("PG_SHOP_IMPORT_NEW", "Import new"))

    d.importMerge = CreateFrame("Button", nil, d, "UIPanelButtonTemplate")
    d.importMerge:SetSize(116, 22)
    d.importMerge:SetPoint("LEFT", d.importNew, "RIGHT", 6, 0)
    d.importMerge:SetText(T("PG_SHOP_IMPORT_MERGE", "Merge"))

    d.selectAll = CreateFrame("Button", nil, d, "UIPanelButtonTemplate")
    d.selectAll:SetSize(92, 22)
    d.selectAll:SetPoint("BOTTOMRIGHT", -104, 8)
    d.selectAll:SetText(T("PG_SHOP_SELECT_ALL", "Select all"))
    d.selectAll:SetScript("OnClick", function()
        d.editBox:SetFocus()
        d.editBox:HighlightText()
    end)

    d.done = CreateFrame("Button", nil, d, "UIPanelButtonTemplate")
    d.done:SetSize(84, 22)
    d.done:SetPoint("BOTTOMRIGHT", -14, 8)
    d.done:SetText(T("PG_SHOP_CLOSE", "Close"))
    d.done:SetScript("OnClick", function()
        d:Hide()
    end)

    local function SetExportText(format)
        local text = S.ExportActiveList and S:ExportActiveList(format) or ""
        d.editBox:SetText(text or "")
        d.editBox:SetFocus()
        d.editBox:HighlightText()
        d.status:SetText(format == "auctionator" and T("PG_SHOP_EXPORT_AUCTIONATOR_READY", "Auctionator export is ready to copy.") or T("PG_SHOP_EXPORT_AHUI_READY", "AhUI export is ready to copy."))
    end

    d.ahui:SetScript("OnClick", function()
        SetExportText("ahui")
    end)

    d.auctionator:SetScript("OnClick", function()
        SetExportText("auctionator")
    end)

    local function Import(mode)
        local value = d.editBox:GetText() or ""
        if S.ImportShoppingLists then
            local ok, err, itemCount, listCount = S:ImportShoppingLists(value, mode)
            if ok then
                d.status:SetText(string.format(T("PG_SHOP_IMPORT_DONE", "Imported: %d items"), tonumber(itemCount) or 0))
                if S.RefreshWindow then
                    S:RefreshWindow()
                end
            else
                d.status:SetText(T("PG_SHOP_IMPORT_FAILED", "Nothing imported. Check the pasted text."))
            end
        end
    end

    d.importNew:SetScript("OnClick", function()
        Import("new")
    end)

    d.importMerge:SetScript("OnClick", function()
        Import("merge")
    end)

    function d:SetMode(mode)
        self.mode = mode
        self.status:SetText("")

        if mode == "export" then
            self.title:SetText(T("PG_SHOP_EXPORT_TITLE", "Export shopping list"))
            self.help:SetText(T("PG_SHOP_EXPORT_HELP", "Copy the text below. AhUI keeps quantities and choices; Auctionator exports item names."))
            self.ahui:Show()
            self.auctionator:Show()
            self.importNew:Hide()
            self.importMerge:Hide()
            SetExportText("ahui")
        else
            self.title:SetText(T("PG_SHOP_IMPORT_TITLE", "Import shopping list"))
            self.help:SetText(T("PG_SHOP_IMPORT_HELP", "Paste AhUI, Auctionator, old Auctionator, TSM, or plain item lines."))
            self.ahui:Hide()
            self.auctionator:Hide()
            self.importNew:Show()
            self.importMerge:Show()
            self.editBox:SetText("")
            self.editBox:SetFocus()
        end
    end

    owner.importExportDialog = d
    return d
end

local function ShowImportExportDialog(mode)
    local f = S.frame
    if not f then
        return
    end

    if f.searchOptionsPanel then
        f.searchOptionsPanel:Hide()
    end
    if f.searchOptionsBlocker then
        f.searchOptionsBlocker:Hide()
    end

    local d = EnsureImportExportDialog(f)
    if not d then
        return
    end

    d:SetMode(mode)
    d:Show()
end


local function RefreshSearchClearButton(frame)
    if not frame or not frame.searchClearButton or not frame.searchBox then
        return
    end

    local hasText = TrimText(frame.searchBox:GetText() or "") ~= ""
    local hasResults = S.searchResults and S.searchResults.active
    local shown = hasText or hasResults

    frame.searchClearButton:SetShown(shown)
    frame.searchClearButton:SetAlpha(shown and 0.85 or 0.25)
end

local function HideSearchOptionsPanel()
    local f = S.frame
    if not f then
        return
    end

    if f.searchOptionsPanel then
        f.searchOptionsPanel:Hide()
    end

    if f.searchOptionsBlocker then
        f.searchOptionsBlocker:Hide()
    end
end

local function EnsureSearchOptionsBlocker(frame)
    if not frame or frame.searchOptionsBlocker then
        return
    end

    local blocker = CreateFrame("Button", nil, frame)
    blocker:SetAllPoints(frame)
    blocker:SetFrameStrata("DIALOG")
    blocker:SetFrameLevel(1)
    blocker:EnableMouse(true)
    blocker:Hide()
    blocker:SetScript("OnMouseDown", function()
        HideSearchOptionsPanel()
    end)

    frame.searchOptionsBlocker = blocker
end

local function RunSearchFromBox()
    local f = S.frame
    if not f or not f.searchBox then return end

    local text = TrimText(f.searchBox:GetText())

    if text == "" and S.CanRunEmptyAuctionSearch and not S:CanRunEmptyAuctionSearch() then
        if S.ClearAuctionSearch then
            S:ClearAuctionSearch()
        end
        if f.hint then
            f.hint:SetText(T("PG_SHOP_SEARCH_NEED_QUERY_OR_FILTER", "Введите текст или выберите тип/качество/уровень"))
        end
        return
    end

    if S.StartAuctionSearch then
        S:StartAuctionSearch(text)
    end

    f.searchBox:SetFocus()
    f.searchBox:SetCursorPosition(strlen(f.searchBox:GetText() or ""))
    RefreshSearchClearButton(f)
end

local function ToggleSearchOptionsPanel(anchor)
    local f = S.frame
    if not f then return end

    if f.searchOptionsPanel and f.searchOptionsPanel:IsShown() then
        HideSearchOptionsPanel()
        return
    end

    EnsureSearchOptionsBlocker(f)

    local function GetOpts()
        if S.GetSearchOptions then
            return S:GetSearchOptions()
        end
        S.searchOptions = S.searchOptions or {}
        return S.searchOptions
    end

    local function SetOpt(key, value)
        if S.SetSearchOption then
            S:SetSearchOption(key, value, true)
        else
            local opts = GetOpts()
            opts[key] = value
        end
    end

    local function MakeCategoryKey(classID, subClassID, inventoryType)
        if not classID then
            return "any"
        end
        return tostring(classID) .. ":" .. tostring(subClassID or "") .. ":" .. tostring(inventoryType or "")
    end

    local function ParseCategoryKey(value)
        if value == "any" then
            return nil, nil, nil
        end

        local classID, subClassID, inventoryType = tostring(value or ""):match("^([^:]*):([^:]*):([^:]*)$")
        classID = tonumber(classID)
        subClassID = tonumber(subClassID)
        inventoryType = tonumber(inventoryType)
        return classID, subClassID, inventoryType
    end

    local function ReadItemClassText(classID, subClassID, fallback)
        if fallback and fallback ~= "" then
            return fallback
        end

        if classID and subClassID and C_Item and C_Item.GetItemSubClassInfo then
            local ok, text = pcall(C_Item.GetItemSubClassInfo, classID, subClassID)
            if ok and text and text ~= "" then
                return text
            end
        end

        if classID and GetItemSubClassInfo and subClassID then
            local ok, text = pcall(GetItemSubClassInfo, classID, subClassID)
            if ok and text and text ~= "" then
                return text
            end
        end

        if classID and C_Item and C_Item.GetItemClassInfo then
            local ok, text = pcall(C_Item.GetItemClassInfo, classID)
            if ok and text and text ~= "" then
                return text
            end
        end

        if classID and GetItemClassInfo then
            local ok, text = pcall(GetItemClassInfo, classID)
            if ok and text and text ~= "" then
                return text
            end
        end

        return classID and tostring(classID) or "?"
    end

    local itemCategoryModel = nil

    local function BuildItemCategoryModel()
        local model = {
            classes = {},
            byID = {},
            subSeen = {},
        }

        local function ReadSubClassText(classID, subClassID)
            classID = tonumber(classID)
            subClassID = tonumber(subClassID)
            if classID == nil or subClassID == nil then
                return nil
            end

            if C_Item and C_Item.GetItemSubClassInfo then
                local ok, text = pcall(C_Item.GetItemSubClassInfo, classID, subClassID)
                if ok and text and text ~= "" then
                    return text
                end
            end

            if GetItemSubClassInfo then
                local ok, text = pcall(GetItemSubClassInfo, classID, subClassID)
                if ok and text and text ~= "" then
                    return text
                end
            end

            return nil
        end

        local function EnsureClass(classID, text)
            classID = tonumber(classID)
            if classID == nil then
                return nil
            end

            local existing = model.byID[classID]
            if existing then
                if (not existing.text or existing.text == "") and text and text ~= "" then
                    existing.text = text
                end
                return existing
            end

            local label = ReadItemClassText(classID, nil, text)
            if not label or label == "" then
                label = tostring(classID)
            end

            local entry = {
                classID = classID,
                text = label,
                subs = {},
            }
            model.byID[classID] = entry
            table.insert(model.classes, entry)
            return entry
        end

        local function AddSubClass(classID, subClassID, inventoryType, text)
            classID = tonumber(classID)
            subClassID = tonumber(subClassID)
            inventoryType = nil
            if classID == nil or subClassID == nil then
                return
            end

            local parent = EnsureClass(classID)
            if not parent then
                return
            end

            local key = MakeCategoryKey(classID, subClassID, inventoryType)
            if model.subSeen[key] then
                return
            end

            local label = ReadSubClassText(classID, subClassID) or text
            if not label or label == "" or label == parent.text then
                return
            end

            model.subSeen[key] = true
            table.insert(parent.subs, {
                classID = classID,
                subClassID = subClassID,
                inventoryType = inventoryType,
                value = key,
                text = label,
            })
        end

        local function Walk(categories, parentClassID)
            for _, cat in ipairs(categories or {}) do
                if type(cat) == "table" then
                    local classID = cat.itemClassID or cat.classID or cat.classId or cat.itemClass or parentClassID
                    local subClassID = cat.itemSubClassID or cat.subClassID or cat.subClassId or cat.itemSubClass
                    local inventoryType = cat.inventoryType or cat.inventorySlotType
                    local text = cat.name or cat.displayName or cat.categoryName or cat.label

                    if subClassID ~= nil then
                        AddSubClass(classID, subClassID, inventoryType, text)
                    else
                        EnsureClass(classID, text)
                    end

                    local childSets = {
                        cat.subCategories,
                        cat.subcategories,
                        cat.childCategories,
                        cat.children,
                        cat.categories,
                    }

                    for _, children in ipairs(childSets) do
                        if type(children) == "table" then
                            Walk(children, classID)
                        end
                    end
                end
            end
        end

        if C_AuctionHouse and C_AuctionHouse.GetAvailableCategories then
            local ok, categories = pcall(C_AuctionHouse.GetAvailableCategories)
            if ok and type(categories) == "table" then
                Walk(categories, nil)
            end
        end

        local fallbackClasses = {
            { 0,  T("PG_SHOP_OPT_CONSUMABLE", "Расходуемые") },
            { 1,  T("PG_SHOP_OPT_CONTAINER", "Сумки") },
            { 2,  T("PG_SHOP_OPT_WEAPON", "Оружие") },
            { 3,  T("PG_SHOP_OPT_GEM", "Самоцветы") },
            { 4,  T("PG_SHOP_OPT_ARMOR", "Броня") },
            { 5,  T("PG_SHOP_OPT_REAGENT", "Реагенты") },
            { 7,  T("PG_SHOP_OPT_TRADE_GOODS", "Ремесленные товары") },
            { 8,  T("PG_SHOP_OPT_ITEM_ENHANCEMENT", "Улучшения") },
            { 9,  T("PG_SHOP_OPT_RECIPE", "Рецепты") },
            { 12, T("PG_SHOP_OPT_QUEST", "Задания") },
            { 15, T("PG_SHOP_OPT_MISC", "Разное") },
            { 16, T("PG_SHOP_OPT_GLYPH", "Символы") },
            { 17, T("PG_SHOP_OPT_BATTLE_PET", "Боевые питомцы") },
        }

        for _, data in ipairs(fallbackClasses) do
            local classID = data[1]
            EnsureClass(classID, data[2])

            for subClassID = 0, 60 do
                local subText = ReadSubClassText(classID, subClassID)
                if subText and subText ~= "" and subText ~= data[2] then
                    AddSubClass(classID, subClassID, nil, subText)
                end
            end
        end

        table.sort(model.classes, function(a, b)
            return tostring(a.text or "") < tostring(b.text or "")
        end)

        for _, class in ipairs(model.classes) do
            table.sort(class.subs, function(a, b)
                return tostring(a.text or "") < tostring(b.text or "")
            end)
        end

        return model
    end

    local function GetItemCategoryModel()
        if not itemCategoryModel then
            itemCategoryModel = BuildItemCategoryModel()
        end
        return itemCategoryModel
    end

    local function BuildItemClassOptions()
        local out = {
            { value = "any", text = T("PG_SHOP_OPT_ANY", "Любой") },
        }

        for _, class in ipairs(GetItemCategoryModel().classes or {}) do
            table.insert(out, {
                value = tostring(class.classID),
                text = class.text,
                classID = class.classID,
            })
        end

        return out
    end

    local function BuildItemSubClassOptions(classID)
        local out = {
            { value = "any", text = T("PG_SHOP_OPT_ANY", "Любой") },
        }

        classID = tonumber(classID)
        if classID == nil then
            return out
        end

        local class = GetItemCategoryModel().byID[classID]
        for _, sub in ipairs(class and class.subs or {}) do
            table.insert(out, {
                value = sub.value,
                text = sub.text,
                classID = sub.classID,
                subClassID = sub.subClassID,
                inventoryType = sub.inventoryType,
            })
        end

        return out
    end

    local itemClassOptions = BuildItemClassOptions()

    local qualityOptions = {
        { value = "any",       text = T("PG_SHOP_OPT_ANY", "Любое") },
        { value = "poor",      text = T("PG_SHOP_QUALITY_POOR", "Низкое") },
        { value = "common",    text = T("PG_SHOP_QUALITY_COMMON", "Обычное") },
        { value = "uncommon",  text = T("PG_SHOP_QUALITY_UNCOMMON", "Необычное") },
        { value = "rare",      text = T("PG_SHOP_QUALITY_RARE", "Редкое") },
        { value = "epic",      text = T("PG_SHOP_QUALITY_EPIC", "Эпическое") },
        { value = "legendary", text = T("PG_SHOP_QUALITY_LEGENDARY", "Легендарное") },
    }

    local sortOptions = {
        { value = "price_asc",  text = T("PG_SHOP_SORT_PRICE_ASC", "Цена: дешёвые") },
        { value = "price_desc", text = T("PG_SHOP_SORT_PRICE_DESC", "Цена: дорогие") },
        { value = "name",       text = T("PG_SHOP_SORT_NAME", "Название") },
        { value = "quantity",   text = T("PG_SHOP_SORT_QUANTITY", "Количество") },
    }

    local expansionOptions = {
        { value = "any", text = T("PG_SHOP_OPT_ANY", "Любое") },
        { value = 0,     text = "Classic" },
        { value = 1,     text = "The Burning Crusade" },
        { value = 2,     text = "Wrath of the Lich King" },
        { value = 3,     text = "Cataclysm" },
        { value = 4,     text = "Mists of Pandaria" },
        { value = 5,     text = "Warlords of Draenor" },
        { value = 6,     text = "Legion" },
        { value = 7,     text = "Battle for Azeroth" },
        { value = 8,     text = "Shadowlands" },
        { value = 9,     text = "Dragonflight" },
        { value = 10,    text = "The War Within" },
        { value = 11,    text = "Midnight" },
    }

    local materialQualityOptions = {
        { value = "any", text = T("PG_SHOP_OPT_ANY", "Любое") },
        { value = 1,     text = "R1" },
        { value = 2,     text = "R2" },
        { value = 3,     text = "R3" },
        { value = 4,     text = "R4" },
        { value = 5,     text = "R5" },
    }

    local function FindOptionText(options, value)
        for _, opt in ipairs(options) do
            if tostring(opt.value) == tostring(value) then
                return opt.text
            end
        end
        return options[1] and options[1].text or ""
    end

    local function MakeLabel(parent, text, x, y)
        local fs = MakeText(parent, 10, "LEFT")
        fs:SetPoint("TOPLEFT", x, y)
        fs:SetText(text)
        return fs
    end

    local function MakeInput(parent, w, key)
        local box = CreateFrame("EditBox", nil, parent, "InputBoxTemplate")
        box:SetAutoFocus(false)
        box:SetFont(FONT, 11, "")
        box:SetSize(w, 20)
        box:SetText(tostring(GetOpts()[key] or ""))

        box:SetScript("OnEnterPressed", function(self)
            SetOpt(key, TrimText(self:GetText()))
            self:ClearFocus()
            RunSearchFromBox()
        end)

        box:SetScript("OnEditFocusLost", function(self)
            SetOpt(key, TrimText(self:GetText()))
        end)

        return box
    end

    local function MakeCheck(parent, text, key, x, y)
        local cb = CreateFrame("CheckButton", nil, parent, "UICheckButtonTemplate")
        cb:SetPoint("TOPLEFT", x, y)
        cb:SetChecked(GetOpts()[key] == true)

        cb.text = MakeText(parent, 10, "LEFT")
        cb.text:SetPoint("LEFT", cb, "RIGHT", -2, 0)
        cb.text:SetText(text)

        cb:SetScript("OnClick", function(self)
            SetOpt(key, self:GetChecked() == true)
            if S.RefreshWindow then
                S:RefreshWindow()
            end
        end)

        return cb
    end

    local function MakeDropdown(parent, w, options, getValue, setValue)
        local function ResolveOptions()
            if type(options) == "function" then
                local ok, result = pcall(options)
                if ok and type(result) == "table" then
                    return result
                end
                return {}
            end
            return options or {}
        end

        if UIDropDownMenu_Initialize and UIDropDownMenu_CreateInfo then
            local dd = CreateFrame("Frame", nil, parent, "UIDropDownMenuTemplate")
            UIDropDownMenu_SetWidth(dd, w)

            function dd:RefreshText()
                UIDropDownMenu_SetText(self, FindOptionText(ResolveOptions(), getValue()))
            end

            dd:RefreshText()

            UIDropDownMenu_Initialize(dd, function()
                local list = ResolveOptions()
                for _, opt in ipairs(list) do
                    local info = UIDropDownMenu_CreateInfo()
                    info.text = opt.text
                    info.checked = tostring(getValue()) == tostring(opt.value)
                    info.func = function()
                        setValue(opt.value, opt)
                        UIDropDownMenu_SetText(dd, opt.text)
                        CloseDropDownMenus()
                        if S.RefreshWindow then
                            S:RefreshWindow()
                        end
                    end
                    UIDropDownMenu_AddButton(info)
                end
            end)

            return dd
        end

        local btn = CreateFrame("Button", nil, parent, "UIPanelButtonTemplate")
        btn:SetSize(w + 30, 20)

        function btn:RefreshText()
            self:SetText(FindOptionText(ResolveOptions(), getValue()))
        end

        btn:SetScript("OnClick", function(self)
            local list = ResolveOptions()
            local current = tostring(getValue())
            local nextIndex = 1

            for index, opt in ipairs(list) do
                if tostring(opt.value) == current then
                    nextIndex = index + 1
                    break
                end
            end

            if nextIndex > #list then
                nextIndex = 1
            end

            local opt = list[nextIndex]
            if not opt then
                return
            end

            setValue(opt.value, opt)
            self:RefreshText()

            if S.RefreshWindow then
                S:RefreshWindow()
            end
        end)

        btn:RefreshText()
        return btn
    end

    local function GetCategoryDropdownText()
        local opts = GetOpts()
        if opts.itemClassID == nil then
            return T("PG_SHOP_OPT_ANY", "Любой")
        end

        local model = GetItemCategoryModel()
        local class = model.byID[tonumber(opts.itemClassID)]
        local classText = class and class.text or tostring(opts.itemClassID)

        if opts.itemSubClassID ~= nil and class then
            for _, sub in ipairs(class.subs or {}) do
                if tonumber(sub.subClassID) == tonumber(opts.itemSubClassID) then
                    return classText .. " > " .. sub.text
                end
            end
        end

        return classText
    end

    local function SetCategoryFilter(classID, subClassID, inventoryType)
        classID = tonumber(classID)
        subClassID = tonumber(subClassID)
        inventoryType = nil

        if classID == nil then
            SetOpt("categoryKey", "any")
            SetOpt("itemClassID", nil)
            SetOpt("itemSubClassID", nil)
            SetOpt("inventoryType", nil)
            return
        end

        SetOpt("itemClassID", classID)
        SetOpt("itemSubClassID", subClassID)
        SetOpt("inventoryType", nil)
        SetOpt("categoryKey", subClassID ~= nil and MakeCategoryKey(classID, subClassID, nil) or tostring(classID))
    end

    local function MakeCategoryDropdown(parent, w)
        if UIDropDownMenu_Initialize and UIDropDownMenu_CreateInfo then
            local dd = CreateFrame("Frame", nil, parent, "UIDropDownMenuTemplate")
            UIDropDownMenu_SetWidth(dd, w)

            function dd:RefreshText()
                UIDropDownMenu_SetText(self, GetCategoryDropdownText())
            end

            UIDropDownMenu_Initialize(dd, function(_, level, menuList)
                level = level or 1

                if level == 1 then
                    local anyInfo = UIDropDownMenu_CreateInfo()
                    anyInfo.text = T("PG_SHOP_OPT_ANY", "Любой")
                    anyInfo.checked = GetOpts().itemClassID == nil
                    anyInfo.func = function()
                        SetCategoryFilter(nil, nil, nil)
                        dd:RefreshText()
                        CloseDropDownMenus()
                        if S.RefreshWindow then S:RefreshWindow() end
                    end
                    UIDropDownMenu_AddButton(anyInfo, level)

                    for _, class in ipairs(GetItemCategoryModel().classes or {}) do
                        local info = UIDropDownMenu_CreateInfo()
                        info.text = class.text
                        info.value = class.classID
                        info.menuList = class.classID
                        info.hasArrow = #(class.subs or {}) > 0
                        info.checked = tonumber(GetOpts().itemClassID) == tonumber(class.classID) and GetOpts().itemSubClassID == nil
                        info.func = function()
                            SetCategoryFilter(class.classID, nil, nil)
                            dd:RefreshText()
                            CloseDropDownMenus()
                            if S.RefreshWindow then S:RefreshWindow() end
                        end
                        UIDropDownMenu_AddButton(info, level)
                    end
                    return
                end

                local classID = tonumber(menuList)
                local class = classID and GetItemCategoryModel().byID[classID]
                if not class then
                    return
                end

                local allInfo = UIDropDownMenu_CreateInfo()
                allInfo.text = T("PG_SHOP_OPT_ANY_SUBTYPE", "Все")
                allInfo.checked = tonumber(GetOpts().itemClassID) == classID and GetOpts().itemSubClassID == nil
                allInfo.func = function()
                    SetCategoryFilter(classID, nil, nil)
                    dd:RefreshText()
                    CloseDropDownMenus()
                    if S.RefreshWindow then S:RefreshWindow() end
                end
                UIDropDownMenu_AddButton(allInfo, level)

                for _, sub in ipairs(class.subs or {}) do
                    local info = UIDropDownMenu_CreateInfo()
                    info.text = sub.text
                    info.checked = tonumber(GetOpts().itemClassID) == tonumber(sub.classID) and tonumber(GetOpts().itemSubClassID) == tonumber(sub.subClassID)
                    info.func = function()
                        SetCategoryFilter(sub.classID, sub.subClassID, nil)
                        dd:RefreshText()
                        CloseDropDownMenus()
                        if S.RefreshWindow then S:RefreshWindow() end
                    end
                    UIDropDownMenu_AddButton(info, level)
                end
            end)

            dd:RefreshText()
            return dd
        end

        local function FlatCategoryOptions()
            local out = {
                { value = "any", text = T("PG_SHOP_OPT_ANY", "Любой") },
            }
            for _, class in ipairs(GetItemCategoryModel().classes or {}) do
                table.insert(out, { value = tostring(class.classID), text = class.text, classID = class.classID })
                for _, sub in ipairs(class.subs or {}) do
                    table.insert(out, { value = sub.value, text = class.text .. " > " .. sub.text, classID = sub.classID, subClassID = sub.subClassID })
                end
            end
            return out
        end

        return MakeDropdown(
            parent,
            w,
            FlatCategoryOptions,
            function()
                local opts = GetOpts()
                if opts.itemClassID == nil then
                    return "any"
                end
                if opts.itemSubClassID ~= nil then
                    return MakeCategoryKey(opts.itemClassID, opts.itemSubClassID, nil)
                end
                return tostring(opts.itemClassID)
            end,
            function(value)
                if value == "any" then
                    SetCategoryFilter(nil, nil, nil)
                    return
                end
                local classID, subClassID = ParseCategoryKey(value)
                SetCategoryFilter(classID or tonumber(value), subClassID, nil)
            end
        )
    end

    if not f.searchOptionsPanel then
        local p = CreateFrame("Frame", nil, f.panel, "BackdropTemplate")
        p:SetSize(430, 314)
        p:SetFrameStrata("DIALOG")
        p:SetFrameLevel(10)
        p:EnableMouse(true)
        SetFrameBackdrop(p, 0.97)
        p:SetScript("OnHide", function()
            if f.searchOptionsBlocker then
                f.searchOptionsBlocker:Hide()
            end
        end)

        p.title = MakeText(p, 12, "LEFT")
        p.title:SetPoint("TOPLEFT", 12, -10)
        p.title:SetText(T("PG_SHOP_SEARCH_OPTIONS", "Расширенный поиск"))

        p.exactMatch = MakeCheck(p, T("PG_SHOP_OPT_EXACT", "Точный поиск"), "exactMatch", 10, -30)
        p.onlyAvailable = MakeCheck(p, T("PG_SHOP_OPT_ONLY_AVAILABLE", "Только доступные"), "onlyAvailable", 150, -30)
        p.hideNoPrice = MakeCheck(p, T("PG_SHOP_OPT_HIDE_NO_PRICE", "Скрыть без цены"), "hideNoPrice", 290, -30)

        MakeLabel(p, T("PG_SHOP_OPT_ITEM_TYPE", "Тип предмета"), 14, -68)
        p.itemClass = MakeCategoryDropdown(p, 150)
        p.itemClass:SetPoint("TOPLEFT", 80, -58)

        MakeLabel(p, T("PG_SHOP_OPT_QUALITY", "Качество"), 250, -68)
        p.quality = MakeDropdown(p, 60, qualityOptions, function() return GetOpts().quality or "any" end, function(value) SetOpt("quality", value) end)
        p.quality:SetPoint("TOPLEFT", 310, -58)

        MakeLabel(p, T("PG_SHOP_OPT_PRICE", "Цена"), 14, -105)
        p.minPrice = MakeInput(p, 40, "minPrice")
        p.minPrice:SetPoint("TOPLEFT", 100, -96)

        p.priceDash = MakeText(p, 10, "CENTER")
        p.priceDash:SetPoint("LEFT", p.minPrice, "RIGHT", 4, 0)
        p.priceDash:SetText("-")

        p.maxPrice = MakeInput(p, 40, "maxPrice")
        p.maxPrice:SetPoint("LEFT", p.priceDash, "RIGHT", 8, 0)

        MakeLabel(p, T("PG_SHOP_OPT_MIN_QTY", "Кол-во от"), 250, -105)
        p.minQuantity = MakeInput(p, 30, "minQuantity")
        p.minQuantity:SetPoint("TOPLEFT", 335, -96)

        MakeLabel(p, T("PG_SHOP_OPT_ITEM_LEVEL", "Ур. предмета"), 14, -140)
        p.minItemLevel = MakeInput(p, 40, "minItemLevel")
        p.minItemLevel:SetPoint("TOPLEFT", 100, -131)

        p.ilvlDash = MakeText(p, 10, "CENTER")
        p.ilvlDash:SetPoint("LEFT", p.minItemLevel, "RIGHT", 4, 0)
        p.ilvlDash:SetText("-")

        p.maxItemLevel = MakeInput(p, 40, "maxItemLevel")
        p.maxItemLevel:SetPoint("LEFT", p.ilvlDash, "RIGHT", 8, 0)

        MakeLabel(p, T("PG_SHOP_OPT_REQUIRED_LEVEL", "Треб. уровень"), 250, -140)
        p.minRequiredLevel = MakeInput(p, 30, "minRequiredLevel")
        p.minRequiredLevel:SetPoint("TOPLEFT", 335, -131)

        p.reqDash = MakeText(p, 10, "CENTER")
        p.reqDash:SetPoint("LEFT", p.minRequiredLevel, "RIGHT", 4, 0)
        p.reqDash:SetText("-")

        p.maxRequiredLevel = MakeInput(p, 30, "maxRequiredLevel")
        p.maxRequiredLevel:SetPoint("LEFT", p.reqDash, "RIGHT", 8, 0)

        MakeLabel(p, T("PG_SHOP_OPT_EXPANSION", "Дополнение"), 14, -177)
        p.expansion = MakeDropdown(p, 100, expansionOptions, function() return GetOpts().expansionID or "any" end, function(value) SetOpt("expansionID", value) end)
        p.expansion:SetPoint("TOPLEFT", 80, -167)

        MakeLabel(p, T("PG_SHOP_OPT_MATERIAL_QUALITY", "Кач-во мат."), 250, -177)
        p.materialQuality = MakeDropdown(p, 70, materialQualityOptions, function() return GetOpts().materialQuality or "any" end, function(value) SetOpt("materialQuality", value) end)
        p.materialQuality:SetPoint("TOPLEFT", 305, -167)

        MakeLabel(p, T("PG_SHOP_OPT_SORT", "Сортировка"), 14, -214)
        p.sortMode = MakeDropdown(p, 100, sortOptions, function() return GetOpts().sortMode or "price_asc" end, function(value) SetOpt("sortMode", value) end)
        p.sortMode:SetPoint("TOPLEFT", 80, -204)

        p.note = MakeText(p, 10, "LEFT")
        p.note:SetPoint("TOPLEFT", 14, -238)
        p.note:SetPoint("RIGHT", -14, 0)
        p.note:SetText(T("PG_SHOP_SEARCH_FILTER_NOTE", "Пустой поиск работает только с типом, качеством или требуемым уровнем."))

        p.reset = CreateFrame("Button", nil, p, "UIPanelButtonTemplate")
        p.reset:SetSize(100, 22)
        p.reset:SetPoint("BOTTOMLEFT", 12, 10)
        p.reset:SetText(T("PG_SHOP_RESET_ALL", "Сбросить"))
        p.reset:SetScript("OnClick", function()
            if S.ResetSearchOptions then
                S:ResetSearchOptions(true)
            end

            HideSearchOptionsPanel()
            f.searchOptionsPanel = nil

            if S.RefreshWindow then
                S:RefreshWindow()
            end
        end)

        p.search = CreateFrame("Button", nil, p, "UIPanelButtonTemplate")
        p.search:SetSize(90, 22)
        p.search:SetPoint("BOTTOMRIGHT", -12, 10)
        p.search:SetText(T("PG_SHOP_SEARCH", "Поиск"))
        p.search:SetScript("OnClick", function()
            RunSearchFromBox()
        end)

        f.searchOptionsPanel = p
    else
        local opts = GetOpts()

        if f.searchOptionsPanel.itemClass and f.searchOptionsPanel.itemClass.RefreshText then f.searchOptionsPanel.itemClass:RefreshText() end
        if f.searchOptionsPanel.quality and f.searchOptionsPanel.quality.RefreshText then f.searchOptionsPanel.quality:RefreshText() end
        if f.searchOptionsPanel.sortMode and f.searchOptionsPanel.sortMode.RefreshText then f.searchOptionsPanel.sortMode:RefreshText() end
        if f.searchOptionsPanel.expansion and f.searchOptionsPanel.expansion.RefreshText then f.searchOptionsPanel.expansion:RefreshText() end
        if f.searchOptionsPanel.materialQuality and f.searchOptionsPanel.materialQuality.RefreshText then f.searchOptionsPanel.materialQuality:RefreshText() end

        if f.searchOptionsPanel.exactMatch then f.searchOptionsPanel.exactMatch:SetChecked(opts.exactMatch == true) end
        if f.searchOptionsPanel.onlyAvailable then f.searchOptionsPanel.onlyAvailable:SetChecked(opts.onlyAvailable == true) end
        if f.searchOptionsPanel.hideNoPrice then f.searchOptionsPanel.hideNoPrice:SetChecked(opts.hideNoPrice == true) end
        if f.searchOptionsPanel.minPrice then f.searchOptionsPanel.minPrice:SetText(tostring(opts.minPrice or "")) end
        if f.searchOptionsPanel.maxPrice then f.searchOptionsPanel.maxPrice:SetText(tostring(opts.maxPrice or "")) end
        if f.searchOptionsPanel.minQuantity then f.searchOptionsPanel.minQuantity:SetText(tostring(opts.minQuantity or "")) end
        if f.searchOptionsPanel.minItemLevel then f.searchOptionsPanel.minItemLevel:SetText(tostring(opts.minItemLevel or "")) end
        if f.searchOptionsPanel.maxItemLevel then f.searchOptionsPanel.maxItemLevel:SetText(tostring(opts.maxItemLevel or "")) end
        if f.searchOptionsPanel.minRequiredLevel then f.searchOptionsPanel.minRequiredLevel:SetText(tostring(opts.minRequiredLevel or "")) end
        if f.searchOptionsPanel.maxRequiredLevel then f.searchOptionsPanel.maxRequiredLevel:SetText(tostring(opts.maxRequiredLevel or "")) end
    end

    f.searchOptionsPanel:ClearAllPoints()
    f.searchOptionsPanel:SetPoint("TOPRIGHT", anchor or f.panel, "BOTTOMRIGHT", 0, -6)

    if f.searchOptionsBlocker then
        f.searchOptionsBlocker:SetFrameStrata("DIALOG")
        f.searchOptionsBlocker:SetFrameLevel(1)
        f.searchOptionsBlocker:Show()
    end

    f.searchOptionsPanel:SetFrameStrata("DIALOG")
    f.searchOptionsPanel:SetFrameLevel(10)
    f.searchOptionsPanel:Show()
end

function S:ClearSearchText(keepResults)
    local f = self.frame
    if f and f.searchBox then
        f.searchBox._ahuiSuppressTextChanged = true
        f.searchBox:SetText("")
        f.searchBox._ahuiSuppressTextChanged = nil
        f.searchBox:ClearFocus()
        RefreshSearchClearButton(f)
    end

    self.activeSearchBox = nil
    self.lastSearchBox = nil
    self.lastSearchBoxUntil = nil
    self.lastInsertedSearchLink = nil
    self.lastInsertedSearchLinkTime = nil

    if not keepResults and self.ClearAuctionSearch then
        self:ClearAuctionSearch()
    end
end

local function AddSelectedSearchResultToList()
    local row = S.selectedSearchResult

    if not row and S.searchResults and S.searchResults.rows then
        for _, candidate in ipairs(S.searchResults.rows or {}) do
            if candidate and candidate.itemID and (not S.SearchResultMatchesOptions or S:SearchResultMatchesOptions(candidate)) then
                row = candidate
                break
            end
        end
    end

    if not row or not row.itemID then
        return
    end

    local quantity = 1
    if row.isCommodity == true and S.frame and S.frame.rows then
        for _, widget in ipairs(S.frame.rows) do
            if widget and widget.hotspot and widget.hotspot.searchRow == row and widget.qtyBox then
                quantity = tonumber(widget.qtyBox:GetText()) or 1
                break
            end
        end
    end

    quantity = math.floor(quantity)
    if quantity <= 0 then
        quantity = 1
    end

    if S.AddItemToActiveList then
        S:AddItemToActiveList(row.itemID, quantity, row.label, row)
        S.selectedSearchResult = nil
        if S.RefreshWindow then
            S:RefreshWindow()
        end
    end
end

local function GetSearchTextFromItemLink(link)
    if type(link) ~= "string" or link == "" then
        return nil
    end

    local itemID = tonumber(link:match("item:(%d+)"))
    local name = GetItemInfo(link)

    if not name and itemID then
        name = GetItemInfo(itemID)
        if not name and C_Item and C_Item.RequestLoadItemDataByID then
            C_Item.RequestLoadItemDataByID(itemID)
        end
    end

    return name or (itemID and ("item:" .. itemID)) or link
end

local function HasActiveChatEditBox()
    local chatBox = ChatEdit_GetActiveWindow and ChatEdit_GetActiveWindow()
    return chatBox and chatBox:IsShown() and chatBox:HasFocus()
end

local function GetSearchBoxForShiftClick()
    local f = S.frame
    if not f or not f:IsShown() or not f.searchBox or not f.searchBox:IsShown() then
        return nil
    end

    if HasActiveChatEditBox() then
        return nil
    end

    if S.activeSearchBox and S.activeSearchBox:IsShown() then
        return S.activeSearchBox
    end

    if f.searchBox:HasFocus() then
        return f.searchBox
    end

    return f.searchBox
end

local function InsertLinkIntoSearchBox(link)
    local box = GetSearchBoxForShiftClick()
    if not box then
        return false
    end

    local text = GetSearchTextFromItemLink(link)
    if not text or text == "" then
        return false
    end

    local now = GetTime and GetTime() or 0
    if S.lastInsertedSearchLink == link and S.lastInsertedSearchLinkTime and (now - S.lastInsertedSearchLinkTime) < 0.25 then
        return true
    end

    S.lastInsertedSearchLink = link
    S.lastInsertedSearchLinkTime = now

    box:SetFocus()
    box:SetText(text)
    box:SetCursorPosition(strlen(text))

    S.activeSearchBox = box
    S.lastSearchBox = box
    S.lastSearchBoxUntil = now + 10

    if S.StartAuctionSearch then
        S:StartAuctionSearch(text)
    end

    C_Timer.After(0, function()
        if box and box:IsShown() then
            box:SetFocus()
            box:SetCursorPosition(strlen(box:GetText() or ""))
        end
    end)

    return true
end

local function InstallSearchBoxLinkHandler()
    if S.searchBoxLinkHandlerInstalled then
        return
    end
    S.searchBoxLinkHandlerInstalled = true

    local oldChatEditInsertLink = ChatEdit_InsertLink
    ChatEdit_InsertLink = function(link, ...)
        if InsertLinkIntoSearchBox(link) then
            return true
        end

        if oldChatEditInsertLink then
            return oldChatEditInsertLink(link, ...)
        end

        return false
    end

    local oldHandleModifiedItemClick = HandleModifiedItemClick
    HandleModifiedItemClick = function(link, ...)
        if link and (IsShiftKeyDown() or (IsModifiedClick and IsModifiedClick("CHATLINK"))) then
            if InsertLinkIntoSearchBox(link) then
                return true
            end
        end

        if oldHandleModifiedItemClick then
            return oldHandleModifiedItemClick(link, ...)
        end

        return false
    end
end

local function PromptRenameList(item)
    if not item or item.kind ~= "saved" or not item.id then
        return
    end

    PromptText(
        "AHUI_SHOPPING_RENAME_LIST",
        T("PG_SHOP_RENAME_LIST", "Rename shopping list"),
        item.name or "",
        function(name)
            if name ~= "" and S.RenameSavedList then
                S:RenameSavedList(item.id, name)
            end
        end
    )
end

local function CreateListButton(parent, index)
    local btn = CreateFrame("Button", nil, parent, "BackdropTemplate")
    btn:SetHeight(26)
    btn:SetWidth(SIDEBAR_W - 16)
    btn:SetPoint("LEFT", 0, 0)
    btn:SetPoint("RIGHT", 0, 0)
    btn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
    SetFrameBackdrop(btn, 0.16)
    btn:SetBackdropBorderColor(1, 1, 1, 0.05)

    btn.name = MakeText(btn, 10, "LEFT")
    btn.name:SetPoint("LEFT", 8, 0)
    btn.name:SetPoint("RIGHT", -28, 0)

    btn.count = MakeText(btn, 10, "RIGHT")
    btn.count:SetPoint("RIGHT", -8, 0)
    btn.count:SetWidth(24)

    btn:SetScript("OnEnter", function(self)
        if self.item then
            ShowSimpleTooltip(self, self.item.name, self.item.kindText)
        end
    end)
    btn:SetScript("OnLeave", function()
        GameTooltip:Hide()
    end)
    btn:SetScript("OnClick", function(self, button)
        local item = self.item
        if not item then return end

        if item.kind == "history" then
            if button == "RightButton" then
                if S.RemoveSearchHistory then
                    S:RemoveSearchHistory(item.query or item.name)
                end
                return
            end

            local f = S.frame
            if f and f.searchBox then
                f.searchBox._ahuiSuppressTextChanged = true
                f.searchBox:SetText(item.query or item.name or "")
                f.searchBox._ahuiSuppressTextChanged = nil
                f.searchBox:SetFocus()
                f.searchBox:SetCursorPosition(strlen(f.searchBox:GetText() or ""))
            end
            RunSearchFromBox()
            return
        end

        if button == "RightButton" then
            PromptRenameList(item)
            return
        end
        if item.kind == "temporary" then
            if S.RefreshWindow then S:RefreshWindow() end
        elseif item.kind == "saved" and S.LoadSavedList then
            S:LoadSavedList(item.id)
        end
    end)

    return btn
end

local function BuildListItems()
    local items = {}
    local mode = S.shoppingSidebarMode or "lists"

    if mode == "history" then
        if S.GetSearchHistory then
            local currentQuery = Lower(S.searchResults and S.searchResults.query or "")
            for _, entry in ipairs(S:GetSearchHistory() or {}) do
                local text = TrimText(entry.text)
                if text ~= "" then
                    table.insert(items, {
                        kind = "history",
                        name = text,
                        query = text,
                        kindText = T("PG_SHOP_HISTORY_ITEM_TOOLTIP", "Left click — search again. Right click — remove from history."),
                        count = (tonumber(entry.count) or 0) > 1 and tostring(entry.count) or "",
                        active = currentQuery ~= "" and Lower(text) == currentQuery,
                    })
                end
            end
        end
        return items
    end

    if S.GetSavedLists then
        for _, list in ipairs(S:GetSavedLists() or {}) do
            table.insert(items, {
                id = list.id,
                kind = "saved",
                name = list.name or list.id,
                kindText = T("PG_SHOP_LIST_SAVED", "Saved list"),
                count = list.count or 0,
                active = S.activeSavedListID == list.id,
            })
        end
    end

    return items
end

local function EnsureListButtons(frame, needed)
    frame.listButtons = frame.listButtons or {}
    local current = #frame.listButtons
    if current >= needed then return end

    for index = current + 1, needed do
        local btn = CreateListButton(frame.listContent, index)
        if index == 1 then
            btn:SetPoint("TOP", frame.listContent, "TOP", 0, 0)
        else
            btn:SetPoint("TOP", frame.listButtons[index - 1], "BOTTOM", 0, -4)
        end
        frame.listButtons[index] = btn
    end
end

local function CreateSidebarTab(parent, text, width)
    local tab = CreateFrame("Button", nil, parent, "BackdropTemplate")
    tab:SetSize(width or SIDEBAR_TAB_W, SIDEBAR_TAB_H)
    tab:RegisterForClicks("LeftButtonUp")
    tab:SetHighlightTexture("Interface\\Buttons\\ButtonHilight-Square", "ADD")
    tab:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8x8",
        edgeFile = "Interface\\Buttons\\WHITE8x8",
        edgeSize = 1,
    })

    tab.label = MakeText(tab, 10, "CENTER")
    tab.label:SetPoint("CENTER", 0, 1)
    tab.label:SetText(text or "")

    tab.bottomCover = tab:CreateTexture(nil, "OVERLAY")
    tab.bottomCover:SetColorTexture(0.05, 0.05, 0.07, 0.95)
    tab.bottomCover:SetPoint("BOTTOMLEFT", 1, -1)
    tab.bottomCover:SetPoint("BOTTOMRIGHT", -1, -1)
    tab.bottomCover:SetHeight(2)
    tab.bottomCover:Hide()

    return tab
end

local function SetSidebarTabVisual(button, active)
    if not button then return end

    if active then
        button:SetBackdropColor(0.05, 0.05, 0.07, 0.62)
        button:SetBackdropBorderColor(0.55, 0.75, 1.0, 0.45)
        button:SetAlpha(1)
        if button.label then
            button.label:SetTextColor(1, 1, 1)
        end
        if button.bottomCover then
            button.bottomCover:Show()
        end
    else
        button:SetBackdropColor(0.02, 0.02, 0.03, 0.34)
        button:SetBackdropBorderColor(1, 1, 1, 0.08)
        button:SetAlpha(1)
        if button.label then
            button.label:SetTextColor(0.62, 0.62, 0.68)
        end
        if button.bottomCover then
            button.bottomCover:Hide()
        end
    end
end

local function RefreshListPanel(frame)
    if not frame then return end

    local mode = S.shoppingSidebarMode or "lists"
    local showLists = mode == "lists"
    local items = BuildListItems()
    local contentWidth = frame.listScroll and frame.listScroll:GetWidth() or 0
    if contentWidth < 20 then
        contentWidth = SIDEBAR_W - 16
    end
    if frame.listContent then
        frame.listContent:SetWidth(contentWidth)
    end

    SetSidebarTabVisual(frame.listsTab, showLists)
    SetSidebarTabVisual(frame.historyTab, not showLists)

    if frame.newList then frame.newList:SetShown(showLists) end
    if frame.saveList then frame.saveList:SetShown(showLists) end
    if frame.deleteList then frame.deleteList:SetShown(showLists) end
    if frame.importList then frame.importList:SetShown(showLists) end
    if frame.exportList then frame.exportList:SetShown(showLists) end
    if frame.clearHistory then
        frame.clearHistory:SetShown(not showLists)
        frame.clearHistory:SetEnabled((not showLists) and #items > 0 and S.ClearSearchHistory ~= nil)
    end

    EnsureListButtons(frame, #items)

    for index, item in ipairs(items) do
        local btn = frame.listButtons[index]
        btn.item = item
        btn:SetWidth(contentWidth)
        btn.name:SetText(item.name or "?")
        btn.count:SetText(tostring(item.count or ""))
        btn:Show()

        if item.active then
            btn:SetBackdropColor(0.18, 0.30, 0.42, 0.60)
            btn:SetBackdropBorderColor(0.55, 0.75, 1.0, 0.45)
        else
            btn:SetBackdropColor(0.05, 0.05, 0.07, 0.16)
            btn:SetBackdropBorderColor(1, 1, 1, 0.05)
        end
    end

    for index = #items + 1, #(frame.listButtons or {}) do
        frame.listButtons[index]:Hide()
        frame.listButtons[index].item = nil
    end

    local contentHeight = math.max(1, #items * 30)
    frame.listContent:SetHeight(contentHeight)

    if frame.deleteList then
        frame.deleteList:SetEnabled(showLists and S.activeSavedListID ~= nil and S.DeleteSavedList ~= nil)
    end
    if frame.saveList then
        local rowCount = #(S:GetRows() or {})
        frame.saveList:SetEnabled(showLists and HasShoppingSession() and S.SaveActiveList ~= nil and (rowCount > 0 or S.activeSavedListID ~= nil))
    end
end

local function IsConsumableOrReagent(rowData)
    local classID = tonumber(rowData and rowData.itemClassID)
    return classID == 0 or classID == 5 or classID == 7
end

local function GetProfessionTierAtlasForRow(rowData, requireChoice)
    if not rowData then
        return nil
    end

    if requireChoice and not IsChoiceRow(rowData) then
        return nil
    end

    local tier = GetTierFromRow(rowData)
    if not tier then
        return nil
    end

    local expansionID = tonumber(rowData.expansionID)
    local isConsumableOrReagent = IsConsumableOrReagent(rowData)

    if expansionID == 11 and isConsumableOrReagent then
        if tier < 1 or tier > 2 then
            return nil
        end
        return "Professions-Icon-Quality-12-Tier" .. tostring(tier)
    end

    if isConsumableOrReagent then
        if tier < 1 or tier > 3 then
            return nil
        end
        return "Professions-Icon-Quality-Tier" .. tostring(tier)
    end

    return "Professions-Icon-Quality-Tier" .. tostring(tier)
end

local function SetNameTierIcon(row, rowData)
    if not row or not row.nameTierIcon then
        return
    end

    local atlas = GetProfessionTierAtlasForRow(rowData, false)
    if not atlas or not row.nameTierIcon.SetAtlas then
        row.nameTierIcon:Hide()
        return
    end

    local ok = pcall(row.nameTierIcon.SetAtlas, row.nameTierIcon, atlas)
    if not ok then
        row.nameTierIcon:Hide()
        return
    end

    row.nameTierIcon:SetSize(NAME_TIER_ICON_SIZE, NAME_TIER_ICON_SIZE)
    row.nameTierIcon:ClearAllPoints()

    local textWidth = row.name and row.name:GetStringWidth() or 0
    local maxWidth = row.name and row.name:GetWidth() or 0
    if maxWidth <= 18 then
        row.nameTierIcon:Hide()
        return
    end

    local x = textWidth + 6
    local maxX = maxWidth - NAME_TIER_ICON_SIZE
    if x > maxX then
        x = maxX
    end
    if x < 0 then
        x = 0
    end

    row.nameTierIcon:SetPoint("LEFT", row.name, "LEFT", x, 0)
    row.nameTierIcon:Show()
end

GetProfessionTierAtlas = function(rowData)
    return GetProfessionTierAtlasForRow(rowData, true)
end
local function CreateRow(parent, index)
    local row = CreateFrame("Frame", nil, parent)
    row:SetHeight(ROW_HEIGHT)
    row:EnableMouse(true)
    row:SetScript("OnMouseDown", function(self)
        if self.searchRow then
            S.selectedSearchResult = self.searchRow
            if S.RefreshWindow then
                S:RefreshWindow()
            end
        end
    end)

    row.bg = row:CreateTexture(nil, "BACKGROUND")
    row.bg:SetAllPoints()
    row.bg:SetColorTexture(1, 1, 1, (index % 2 == 0) and 0.035 or 0.02)

    row.icon = row:CreateTexture(nil, "ARTWORK")
    row.icon:SetSize(20, 20)
    row.icon:SetPoint("LEFT", 6, 0)

    row.name = MakeText(row, 11, "LEFT")
    row.name:SetPoint("LEFT", row.icon, "RIGHT", 8, 0)
    row.name:SetPoint("RIGHT", row, "RIGHT", -420, 0)

    row.nameTierIcon = row:CreateTexture(nil, "OVERLAY")
    row.nameTierIcon:SetSize(NAME_TIER_ICON_SIZE, NAME_TIER_ICON_SIZE)
    row.nameTierIcon:Hide()

    row.qtyBox = CreateFrame("EditBox", nil, row, "InputBoxTemplate")
    row.qtyBox:SetSize(QTY_W, 20)
    row.qtyBox:SetPoint("RIGHT", -(ACTION_W + STATUS_W + AH_W + PRICE_W + CHOSEN_W + 40), 0)
    row.qtyBox:SetAutoFocus(false)
    row.qtyBox:SetNumeric(true)
    row.qtyBox:SetJustifyH("CENTER")
    row.qtyBox:SetFont(FONT, 11, "")
    row.qtyBox:SetScript("OnEnterPressed", ApplyQuantityFromBox)
    row.qtyBox:SetScript("OnEscapePressed", function(self)
        if self.searchRow and self.searchRow.isSearchResult then
            if self.searchRow.isCommodity == true then
                local current = GetRemainingQuantity(self.searchRow)
                self:SetText(tostring(current > 0 and current or 1))
            else
                self:SetText("1")
            end
            self:ClearFocus()
            return
        end

        local rowData = self.rowIndex and S.session and S.session.rows and S.session.rows[self.rowIndex]
        self:SetText(tostring(GetRemainingQuantity(rowData)))
        self:ClearFocus()
    end)
    row.qtyBox:SetScript("OnEditFocusLost", ApplyQuantityFromBox)
    row.qtyBox:SetScript("OnEnter", function(self)
        ShowSimpleTooltip(self, T("PG_SHOP_COL_QUANTITY", "Quantity"), T("PG_SHOP_QTY_TOOLTIP", "Edit the quantity and press Enter."))
    end)
    row.qtyBox:SetScript("OnLeave", function()
        GameTooltip:Hide()
    end)

    row.name:ClearAllPoints()
    row.name:SetPoint("LEFT", row.icon, "RIGHT", 8, 0)
    row.name:SetPoint("RIGHT", row.qtyBox, "LEFT", -12, 0)

    row.chosen = MakeText(row, 11, "CENTER")
    row.chosen:SetPoint("LEFT", row.qtyBox, "RIGHT", 0, 0)
    row.chosen:SetWidth(CHOSEN_W)

    row.chosenIcon = row:CreateTexture(nil, "OVERLAY")
    row.chosenIcon:SetSize(CHOSEN_TIER_ICON_SIZE, CHOSEN_TIER_ICON_SIZE)
    row.chosenIcon:SetPoint("CENTER", row.chosen, "CENTER", 0, 0)
    row.chosenIcon:Hide()

    row.price = MakeText(row, 11, "RIGHT")
    row.price:SetPoint("RIGHT", row, "RIGHT", -188, 0)
    row.price:SetWidth(PRICE_W)

    row.available = MakeText(row, 11, "RIGHT")
    row.available:SetPoint("RIGHT", row, "RIGHT", -132, 0)
    row.available:SetWidth(AH_W)

    row.status = MakeText(row, 10, "RIGHT")
    row.status:SetPoint("LEFT", row.available, "RIGHT", 0, 0)
    row.status:SetWidth(STATUS_W)

    row.buyBtn = CreateIconButton(row, "Perks-ShoppingCart", T("PG_SHOP_BUY", "Buy"), T("PG_SHOP_BUY_TOOLTIP", "Start purchase for this item."), BUY_BTN_SIZE, BUY_ICON_SIZE)
    row.buyBtn:SetSize(BUY_BTN_SIZE, BUY_BTN_SIZE)
    row.buyBtn.icon:SetSize(BUY_ICON_SIZE, BUY_ICON_SIZE)
    row.buyBtn.iconSize = BUY_ICON_SIZE
    row.buyBtn:SetPoint("RIGHT", 2, 0)
    row.buyBtn.rowFrame = row
    row.buyBtn:SetScript("OnClick", function(self)
        if self.mode == "buy_search_result" then
            local qty = 1
            if self.rowFrame and self.rowFrame.qtyBox then
                qty = tonumber(self.rowFrame.qtyBox:GetText()) or 1
            end
            if self.searchRow and S.BuySearchResultRow then
                S:BuySearchResultRow(self.searchRow, qty)
            elseif self.itemID and S.BuySearchResult then
                S:BuySearchResult(self.itemID, qty, self.itemName)
            end
            return
        end

        if self.rowIndex then
            S:BuyRow(self.rowIndex)
        end
    end)

    row.scanBtn = CreateIconButton(row, "uitools-icon-refresh", T("PG_SHOP_RESCAN", "Rescan"), T("PG_SHOP_RESCAN_TOOLTIP", "Refresh price for this item."))
    row.scanBtn:SetSize(18, 18)
    row.scanBtn.icon:SetSize(18, 18)
    row.scanBtn:SetPoint("RIGHT", row.buyBtn, "LEFT", 2, 0)
    row.scanBtn:SetScript("OnClick", function(self)
        if self.rowIndex then
            S:ScanRow(self.rowIndex)
        end
    end)

    row.deleteBtn = CreateIconButton(row, "common-icon-redx", T("PG_SHOP_DELETE", "Delete"), T("PG_SHOP_DELETE_ROW_TOOLTIP", "Remove this item from the shopping list."))
    row.deleteBtn:SetSize(18, 18)
    row.deleteBtn.icon:SetSize(16, 16)
    row.deleteBtn:SetPoint("RIGHT", row.scanBtn, "LEFT", 0, 0)
    row.deleteBtn:SetScript("OnClick", function(self)
        if self.rowIndex and S.RemoveRow then
            S:RemoveRow(self.rowIndex)
        end
    end)

    row.hotspot = CreateFrame("Frame", nil, row)
    row.hotspot:SetPoint("TOPLEFT", row, "TOPLEFT", 0, 0)
    row.hotspot:SetPoint("BOTTOMRIGHT", row.qtyBox, "BOTTOMLEFT", -4, 0)
    row.hotspot:EnableMouse(true)
    row.hotspot:SetFrameLevel(row:GetFrameLevel() + 5)
    row.hotspot.rowFrame = row
    row.hotspot:SetScript("OnEnter", function(self)
        if self.itemID then
            ShowItemTooltip(self, self.itemID)
        else
            ShowRowTooltip(self.rowFrame)
        end
    end)
    row.hotspot:SetScript("OnLeave", function()
        GameTooltip:Hide()
    end)
    row.hotspot:SetScript("OnMouseDown", function(self)
        if self.searchRow then
            S.selectedSearchResult = self.searchRow
            if S.RefreshWindow then
                S:RefreshWindow()
            end
        end
    end)

    return row
end

local function ConfigureRowHotspot(row, isSearchResult)
    if not row or not row.hotspot then
        return
    end

    row.hotspot:ClearAllPoints()

    if isSearchResult then
        row.hotspot:SetPoint("TOPLEFT", row, "TOPLEFT", 0, 0)
        row.hotspot:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", 0, 0)
        local inputLevel = row.hotspot:GetFrameLevel() + 1
        if row.qtyBox then
            row.qtyBox:SetFrameLevel(inputLevel)
        end
        if row.buyBtn then
            row.buyBtn:SetFrameLevel(inputLevel)
        end
        if row.scanBtn then
            row.scanBtn:SetFrameLevel(inputLevel)
        end
        if row.deleteBtn then
            row.deleteBtn:SetFrameLevel(inputLevel)
        end
    else
        row.hotspot:SetPoint("TOPLEFT", row, "TOPLEFT", 0, 0)
        row.hotspot:SetPoint("BOTTOMRIGHT", row.qtyBox, "BOTTOMLEFT", -4, 0)
        if row.qtyBox then
            row.qtyBox:SetFrameLevel(row:GetFrameLevel() + 1)
        end
    end
end

local function EnsureVisibleRows(frame, needed)
    frame.rows = frame.rows or {}
    local current = #frame.rows
    if current >= needed then
        return
    end

    for index = current + 1, needed do
        local row = CreateRow(frame.content, index)
        if index == 1 then
            row:SetPoint("TOPLEFT", 0, 0)
            row:SetPoint("TOPRIGHT", -4, 0)
        else
            row:SetPoint("TOPLEFT", frame.rows[index - 1], "BOTTOMLEFT", 0, -ROW_GAP)
            row:SetPoint("TOPRIGHT", frame.rows[index - 1], "BOTTOMRIGHT", 0, -ROW_GAP)
        end
        frame.rows[index] = row
    end
end

function S:CreateWindow()
    if self.frame then
        return self.frame
    end

    local parent = AuctionHouseFrame or UIParent

    local f = CreateFrame("Frame", "AhUIShoppingListTab", parent)
    f:SetAllPoints(parent)
    f:Hide()
    f:SetScript("OnShow", function()
        if S.searchResults and not S.searchResults.active and f.searchBox then
            S:ClearSearchText(true)
        end
        RefreshSearchClearButton(f)
        S:RefreshWindow()
    end)
    f:SetScript("OnHide", function()
        HideSearchOptionsPanel()
        S:ClearSearchText(true)
    end)

    f.panel = CreateFrame("Frame", nil, f, "BackdropTemplate")
    f.panel:SetPoint("TOPLEFT", 10, -84)
    f.panel:SetPoint("BOTTOMRIGHT", -30, 30)
    SetFrameBackdrop(f.panel, 0.72)
    SetFrameAtlasBackground(f.panel, "shop-bg-toys", 0.60)

    f.searchLabel = MakeText(f.panel, 10, "LEFT")
    f.searchLabel:SetPoint("TOPLEFT", PANEL_PAD, -12)
    f.searchLabel:SetText(T("PG_SHOP_SEARCH", "Search"))

    f.searchBox = CreateFrame("EditBox", nil, f.panel, "InputBoxTemplate")
    f.searchBox:SetAutoFocus(false)
    f.searchBox:SetFont(FONT, 11, "")
    f.searchBox:SetHeight(22)
    f.searchBox:EnableMouse(true)
    f.searchBox:EnableKeyboard(true)
    if f.searchBox.SetTextInsets then
        f.searchBox:SetTextInsets(0, 18, 0, 0)
    end

    f.searchClearButton = CreateIconButton(f.searchBox, "common-icon-redx", T("PG_SHOP_CLEAR_SEARCH", "Clear search"), T("PG_SHOP_CLEAR_SEARCH_TOOLTIP", "Clear the search field."), 16, 12)
    f.searchClearButton:SetPoint("RIGHT", f.searchBox, "RIGHT", -2, 0)
    f.searchClearButton:SetFrameLevel(f.searchBox:GetFrameLevel() + 8)
    f.searchClearButton:SetScript("OnClick", function()
        if S.ClearSearchText then
            S:ClearSearchText(false)
        else
            f.searchBox:SetText("")
            if S.ClearAuctionSearch then
                S:ClearAuctionSearch()
            end
            f.searchBox:ClearFocus()
        end
        RefreshSearchClearButton(f)
    end)
    f.searchClearButton:Hide()

    f.addToListButton = CreateIconButton(f.panel, "communities-chat-icon-plus", T("PG_SHOP_ADD_TO_LIST", "Add to list"), T("PG_SHOP_ADD_TO_LIST_TOOLTIP", "Add selected search result to the shopping list."), 24, 24)
    f.addToListButton:SetPoint("TOPRIGHT", -PANEL_PAD, -8)
    f.addToListButton:SetSize(20, 20)
    f.addToListButton:SetScript("OnClick", AddSelectedSearchResultToList)
    f.addToListButton:Show()

    f.searchOptionsButton = CreateIconButton(f.panel, "GM-icon-settings-hover", T("PG_SHOP_SEARCH_OPTIONS", "Search options"), T("PG_SHOP_SEARCH_OPTIONS_TOOLTIP", "Open search parameters."), 30, 30)
    f.searchOptionsButton:SetPoint("RIGHT", f.addToListButton, "LEFT", -SEARCH_BTN_GAP, 0)
    f.searchOptionsButton:SetSize(20, 20)
    f.searchOptionsButton:SetScript("OnClick", function(self)
        ToggleSearchOptionsPanel(self)
    end)
    f.searchOptionsButton:Show()

    f.searchButton = CreateIconButton(f.panel, "talents-search-exactmatch", T("PG_SHOP_SEARCH", "Search"), T("PG_SHOP_SEARCH_TOOLTIP", "Search auction house."), 30, 30)
    f.searchButton:SetPoint("RIGHT", f.searchOptionsButton, "LEFT", -SEARCH_BTN_GAP, 0)
    f.searchButton:SetSize(20, 20)
    f.searchButton:SetScript("OnClick", RunSearchFromBox)
    f.searchButton:Show()

    f.searchBox:SetPoint("LEFT", f.searchLabel, "RIGHT", 10, 0)
    f.searchBox:SetPoint("RIGHT", f.searchButton, "LEFT", -6, 0)
    InstallSearchBoxLinkHandler()

    f.searchBox:SetScript("OnEditFocusGained", function(self)
        S.activeSearchBox = self
        S.lastSearchBox = self
        S.lastSearchBoxUntil = (GetTime and GetTime() or 0) + 10
    end)

    f.searchBox:SetScript("OnEditFocusLost", function(self)
        if S.activeSearchBox == self then
            S.activeSearchBox = nil
        end
        S.lastSearchBox = self
        S.lastSearchBoxUntil = (GetTime and GetTime() or 0) + 10
    end)

    f.searchBox:SetScript("OnTextChanged", function(self)
        if self._ahuiSuppressTextChanged then
            return
        end

        RefreshSearchClearButton(f)

        local text = TrimText(self:GetText())
        if text == "" and S.CanRunEmptyAuctionSearch and not S:CanRunEmptyAuctionSearch() and S.ClearAuctionSearch then
            S:ClearAuctionSearch(true)
        end
    end)

    f.searchBox:SetScript("OnEnterPressed", function(self)
        RunSearchFromBox()
        self:ClearFocus()
    end)
    f.searchBox:SetScript("OnEscapePressed", function(self)
        if S.ClearSearchText then
            S:ClearSearchText(false)
        else
            self:SetText("")
            if S.ClearAuctionSearch then
                S:ClearAuctionSearch()
            end
            self:ClearFocus()
        end
    end)

    f.hint = MakeText(f.panel, 10, "RIGHT")
    f.hint:SetPoint("TOPRIGHT", f.panel, "TOPRIGHT", -PANEL_PAD, -34)
    f.hint:SetWidth(260)
    f.hint:SetText("")

    f.sidebar = CreateFrame("Frame", nil, f.panel, "BackdropTemplate")
    f.sidebar:SetPoint("TOPLEFT", PANEL_PAD, -(TOP_H + 10))
    f.sidebar:SetPoint("BOTTOMLEFT", PANEL_PAD, 36)
    f.sidebar:SetWidth(SIDEBAR_W)
    SetFrameBackdrop(f.sidebar, 0.36)

    f.listsTab = CreateSidebarTab(f.sidebar, T("PG_SHOP_TAB_LISTS", "Lists"), SIDEBAR_TAB_W)
    f.listsTab:SetPoint("TOPLEFT", 8, -7)
    f.listsTab:SetScript("OnClick", function()
        S.shoppingSidebarMode = "lists"
        if S.RefreshWindow then S:RefreshWindow() end
    end)

    f.historyTab = CreateSidebarTab(f.sidebar, T("PG_SHOP_TAB_SEARCH_HISTORY", "History"), SIDEBAR_TAB_W)
    f.historyTab:SetPoint("LEFT", f.listsTab, "RIGHT", SIDEBAR_TAB_GAP, 0)
    f.historyTab:SetScript("OnClick", function()
        S.shoppingSidebarMode = "history"
        if S.RefreshWindow then S:RefreshWindow() end
    end)

    f.newList = CreateFrame("Button", nil, f.sidebar, "UIPanelButtonTemplate")
    f.newList:SetSize(46, 20)
    f.newList:SetPoint("TOPLEFT", 8, -36)
    f.newList:SetText(T("PG_SHOP_NEW_LIST_SHORT", "New"))
    f.newList:SetScript("OnClick", function()
        PromptText(
            "AHUI_SHOPPING_NEW_LIST",
            T("PG_SHOP_NEW_LIST", "New shopping list"),
            "",
            function(name)
                if S.CreateSavedList then
                    S:CreateSavedList(name)
                elseif S.CreateManualList then
                    S:CreateManualList(name)
                end
            end
        )
    end)

    f.saveList = CreateFrame("Button", nil, f.sidebar, "UIPanelButtonTemplate")
    f.saveList:SetSize(54, 20)
    f.saveList:SetPoint("LEFT", f.newList, "RIGHT", 4, 0)
    f.saveList:SetText(T("PG_SHOP_SAVE_LIST_SHORT", "Save"))
    f.saveList:SetScript("OnClick", function()
        local currentName = S.GetActiveListName and S:GetActiveListName() or T("PG_SHOP_SAVED_LIST_DEFAULT", "Shopping list")
        PromptText(
            "AHUI_SHOPPING_SAVE_LIST",
            T("PG_SHOP_SAVE_LIST", "Save shopping list"),
            currentName,
            function(name)
                if name == "" then
                    name = currentName
                end
                if S.activeSavedListID and #(S:GetRows() or {}) == 0 and S.RenameSavedList then
                    S:RenameSavedList(S.activeSavedListID, name)
                elseif S.SaveActiveList then
                    S:SaveActiveList(name)
                end
            end
        )
    end)

    f.deleteList = CreateFrame("Button", nil, f.sidebar, "UIPanelButtonTemplate")
    f.deleteList:SetSize(54, 20)
    f.deleteList:SetPoint("LEFT", f.saveList, "RIGHT", 4, 0)
    f.deleteList:SetText(T("PG_SHOP_DELETE_LIST_SHORT", "Del"))
    f.deleteList:SetScript("OnClick", function()
        if S.activeSavedListID and S.DeleteSavedList then
            S:DeleteSavedList(S.activeSavedListID)
        end
    end)

    f.importList = CreateFrame("Button", nil, f.sidebar, "UIPanelButtonTemplate")
    f.importList:SetSize(76, 20)
    f.importList:SetPoint("TOPLEFT", 8, -60)
    f.importList:SetText(T("PG_SHOP_IMPORT_SHORT", "Import"))
    f.importList:SetScript("OnClick", function()
        ShowImportExportDialog("import")
    end)

    f.exportList = CreateFrame("Button", nil, f.sidebar, "UIPanelButtonTemplate")
    f.exportList:SetSize(76, 20)
    f.exportList:SetPoint("LEFT", f.importList, "RIGHT", 4, 0)
    f.exportList:SetText(T("PG_SHOP_EXPORT_SHORT", "Export"))
    f.exportList:SetScript("OnClick", function()
        ShowImportExportDialog("export")
    end)

    f.clearHistory = CreateFrame("Button", nil, f.sidebar, "UIPanelButtonTemplate")
    f.clearHistory:SetSize(154, 20)
    f.clearHistory:SetPoint("TOPLEFT", 8, -36)
    f.clearHistory:SetText(T("PG_SHOP_CLEAR_HISTORY", "Clear history"))
    f.clearHistory:SetScript("OnClick", function()
        if S.ClearSearchHistory then
            S:ClearSearchHistory()
        end
    end)
    f.clearHistory:Hide()

    f.listScroll = CreateFrame("ScrollFrame", nil, f.sidebar)
    f.listScroll:SetPoint("TOPLEFT", 8, -88)
    f.listScroll:SetPoint("BOTTOMRIGHT", -8, 8)
    f.listContent = CreateFrame("Frame", nil, f.listScroll)
    f.listContent:SetSize(SIDEBAR_W - 16, 1)
    f.listScroll:SetScrollChild(f.listContent)
    f.listButtons = {}

    f.main = CreateFrame("Frame", nil, f.panel, "BackdropTemplate")
    f.main:SetPoint("TOPLEFT", f.sidebar, "TOPRIGHT", 10, 0)
    f.main:SetPoint("BOTTOMRIGHT", -PANEL_PAD, 36)
    SetFrameBackdrop(f.main, 0.28)

    f.header = CreateFrame("Frame", nil, f.main, "BackdropTemplate")
    f.header:SetPoint("TOPLEFT", 8, -8)
    f.header:SetPoint("TOPRIGHT", -8, -8)
    f.header:SetHeight(HEADER_H)
    f.header:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8x8" })
    f.header:SetBackdropColor(1, 1, 1, 0.05)

    local function MakeHeader(text, point, relativeTo, relativePoint, x, w, justify)
        local fs = MakeText(f.header, 10, justify or "LEFT")
        fs:SetPoint(point, relativeTo, relativePoint, x, 0)
        fs:SetWidth(w)
        fs:SetText(text)
        return fs
    end

    f.hItem = MakeHeader(T("PG_SHOP_COL_ITEM", "Item"), "LEFT", f.header, "LEFT", 35, 160, "LEFT")
    f.hAction = MakeHeader("", "RIGHT", f.header, "RIGHT", 17, ACTION_W + 20, "RIGHT")
    f.hStatus = MakeHeader(T("PG_SHOP_COL_STATUS", "Status"), "RIGHT", f.header, "RIGHT", -60, STATUS_W, "RIGHT")
    f.hAH = MakeHeader(T("PG_SHOP_COL_AH", "On AH"), "RIGHT", f.header, "RIGHT", -132, AH_W, "RIGHT")
    f.hPrice = MakeHeader(T("PG_SHOP_COL_PRICE", "Price"), "RIGHT", f.header, "RIGHT", -188, PRICE_W, "RIGHT")
    f.hChosen = MakeHeader(T("PG_SHOP_COL_SELECTED", "Sel."), "RIGHT", f.hPrice, "LEFT", 0, CHOSEN_W, "CENTER")
    f.hQty = MakeHeader(T("PG_SHOP_COL_QUANTITY", "Quantity"), "RIGHT", f.hChosen, "LEFT", 0, QTY_W, "CENTER")

    f.scroll = CreateFrame("ScrollFrame", nil, f.main)
    f.scroll:SetPoint("TOPLEFT", f.header, "BOTTOMLEFT", 0, -4)
    f.scroll:SetPoint("BOTTOMRIGHT", f.main, "BOTTOMRIGHT", -8, 8)
    f.scroll:EnableMouseWheel(true)
    f.scroll:SetScript("OnMouseWheel", function(self, delta)
        local current = self:GetVerticalScroll() or 0
        local step = (ROW_HEIGHT + ROW_GAP) * 2
        local child = self:GetScrollChild()
        local maxScroll = math.max(0, (child and child:GetHeight() or 0) - self:GetHeight())
        local target = current - (delta * step)
        if target < 0 then target = 0 end
        if target > maxScroll then target = maxScroll end
        self:SetVerticalScroll(target)
    end)

    f.content = CreateFrame("Frame", nil, f.scroll)
    f.content:SetSize(1, 1)
    f.scroll:SetScrollChild(f.content)
    f.rows = {}

    f.empty = MakeText(f.main, 11, "CENTER")
    f.empty:SetPoint("CENTER", 0, -8)
    f.empty:SetText(T("PG_SHOP_EMPTY", "List is empty"))
    f.empty:Hide()

    f.total = MakeText(f.panel, 11, "LEFT")
    f.total:SetPoint("BOTTOMLEFT", PANEL_PAD, 12)
    f.total:SetPoint("RIGHT", -PANEL_PAD, 0)
    f.total:SetText("")

    f.refreshAll = CreateFrame("Button", nil, f.panel, "UIPanelButtonTemplate")
    f.refreshAll:SetSize(112, 20)
    f.refreshAll:SetPoint("BOTTOMRIGHT", -PANEL_PAD, 8)
    f.refreshAll:SetText(T("PG_SHOP_RESET_PRICES", "Reset prices"))
    f.refreshAll:SetScript("OnClick", function()
        S:ResetPrices()
    end)

    f.craftSimIntercept = CreateCheckLine(
        f.panel,
        T("PG_SHOP_CRAFTSIM_INTERCEPT", "CraftSim intercept"),
        T("PG_SHOP_CRAFTSIM_INTERCEPT", "CraftSim intercept"),
        T("PG_SHOP_CRAFTSIM_INTERCEPT_TOOLTIP", "Intercept CraftSim Auctionator shopping lists into AhUIshop."),
        function()
            if S.IsCraftSimInterceptEnabled then
                return S:IsCraftSimInterceptEnabled()
            end
            return PT.config and PT.config.shoppingListInterceptCraftSim == true
        end,
        function(value)
            if S.SetCraftSimInterceptEnabled then
                S:SetCraftSimInterceptEnabled(value)
            else
                PT.config = PT.config or {}
                PT.config.shoppingListInterceptCraftSim = value == true
            end
        end
    )
    f.craftSimIntercept:SetPoint("RIGHT", f.refreshAll, "LEFT", -10, 0)

    f.scanAll = CreateFrame("Button", nil, f.panel, "UIPanelButtonTemplate")
    f.scanAll:SetSize(1, 1)
    f.scanAll:SetPoint("RIGHT", f.refreshAll, "LEFT", 0, 0)
    f.scanAll:SetText("")
    f.scanAll:Hide()

    f.buyAll = CreateFrame("Button", nil, f.panel, "UIPanelButtonTemplate")
    f.buyAll:SetSize(1, 1)
    f.buyAll:SetPoint("RIGHT", f.scanAll, "LEFT", 0, 0)
    f.buyAll:SetText("")
    f.buyAll:Hide()

    self.frame = f
    return f
end

function S:EnsureAuctionTab()
    local tabLib = GetLibAHTab()
    if not tabLib or not AuctionHouseFrame then
        return nil
    end

    if not ShouldShowAuctionTab() then
        return nil
    end

    local f = self:CreateWindow()

    if f:GetParent() ~= AuctionHouseFrame then
        f:SetParent(AuctionHouseFrame)
        f:ClearAllPoints()
        f:SetAllPoints(AuctionHouseFrame)
    end

    if not tabLib:DoesIDExist(AH_TAB_ID) then
        tabLib:CreateTab(AH_TAB_ID, f, T(AH_TAB_TEXT_KEY, "Shopping"), T(TITLE, "AhUI: shopping list"))
    end

    UpdateAuctionTabVisibility()

    return f
end

function S:ShowWindow()
    local tabLib = GetLibAHTab()
    local f = self:CreateWindow()

    if AuctionHouseFrame and tabLib and HasShoppingSession() then
        self.isAuctionHouseOpen = true

        f:SetParent(AuctionHouseFrame)
        f:ClearAllPoints()
        f:SetAllPoints(AuctionHouseFrame)

        self:EnsureAuctionTab()
        self:RefreshWindow()
        if f.scroll and not f._didInitialScrollReset then
            f.scroll:SetVerticalScroll(0)
            f._didInitialScrollReset = true
        end

        if tabLib:DoesIDExist(AH_TAB_ID) then
            tabLib:SetSelected(AH_TAB_ID)
        end
    else
        f:Hide()
    end
end

function S:HideWindow()
    if self.frame then
        self.frame:Hide()
    end
end

local function GetDisplayedSearchRows(rows)
    rows = rows or {}
    local out = {}

    for _, row in ipairs(rows) do
        if not S.SearchResultMatchesOptions or S:SearchResultMatchesOptions(row) then
            table.insert(out, row)
        end
    end

    return out
end

function S:RefreshWindow()
    local f = self.frame
    if not f then
        return
    end

    local savedScroll = f.scroll and f.scroll:GetVerticalScroll() or 0
    local allRows = self:GetRows()
    local showingSearchResults = self.searchResults and self.searchResults.active
    local rows = showingSearchResults and GetDisplayedSearchRows(self.searchResults.rows or {}) or GetVisibleRows()
    local total = #rows

    RequestSessionItemData()
    RefreshListPanel(f)

    f.empty:SetShown(total == 0)
    EnsureVisibleRows(f, total)

    if f.scroll and f.content then
        local contentWidth = math.max(1, f.scroll:GetWidth() or 1)
        f.content:SetWidth(contentWidth)
    end

    for index, rowData in ipairs(rows) do
        local row = f.rows[index]
        row:Show()
        row.rowIndex = rowData.index
        row.scanBtn.rowIndex = rowData.index
        row.buyBtn.rowIndex = rowData.index
        row.qtyBox.rowIndex = rowData.index
        row.qtyBox.searchRow = nil
        row.buyBtn.mode = nil
        row.buyBtn.searchRow = nil
        row.buyBtn.itemID = nil
        row.scanBtn.itemID = nil
        if row.deleteBtn then
            row.deleteBtn.rowIndex = rowData.index
        end

        row.searchRow = rowData.isSearchResult and rowData or nil

        if row.hotspot then
            ConfigureRowHotspot(row, rowData.isSearchResult == true)
            row.hotspot.rowIndex = rowData.index
            row.hotspot.itemID = rowData.isSearchResult and rowData.itemID or nil
            row.hotspot.searchRow = rowData.isSearchResult and rowData or nil
        end

        local iconID = GetDisplayItemID(rowData)
        row.icon:SetTexture(GetItemIcon(iconID))
        row.name:SetText(GetRowDisplayName(rowData))
        ApplyNameColor(row, rowData)
        SetNameTierIcon(row, rowData)

        local remaining = GetRemainingQuantity(rowData)
        if not row.qtyBox:HasFocus() then
            row.qtyBox:SetText(tostring(remaining))
        end

        local qtyEditable = IsQuantityEditable(rowData)
        row.qtyBox:SetEnabled(qtyEditable)
        row.qtyBox:SetAlpha(qtyEditable and 1 or 0.45)

        SetChosenDisplay(row, rowData)
        row.price:SetText(FormatMoneyText(rowData.pendingUnitPrice or rowData.minPrice))
        row.available:SetText(rowData.totalQuantity ~= nil and tostring(rowData.totalQuantity) or T("PG_DASH", "—"))
        row.status:SetText(rowData.status or "")
        row.status:SetTextColor(1, 1, 1)
        row.bg:SetColorTexture(1, 1, 1, (index % 2 == 0) and 0.035 or 0.02)

        if rowData.isSearchResult and S.selectedSearchResult and (S.selectedSearchResult == rowData or (S.selectedSearchResult.seenKey and rowData.seenKey and S.selectedSearchResult.seenKey == rowData.seenKey)) then
            row.bg:SetColorTexture(0.25, 0.45, 0.70, 0.32)
        end

        if rowData.isSearchResult then
            local displayRow = rowData
            local displayQty = GetRemainingQuantity(displayRow)

            row.qtyBox.rowIndex = nil
            row.qtyBox.searchRow = rowData
            qtyEditable = IsQuantityEditable(rowData)
            row.qtyBox:SetEnabled(qtyEditable)
            row.qtyBox:SetAlpha(qtyEditable and 1 or 0.45)

            if not row.qtyBox:HasFocus() then
                if rowData.isCommodity == true then
                    row.qtyBox:SetText(tostring(displayQty > 0 and displayQty or (rowData.quantity or 1)))
                else
                    row.qtyBox:SetText("1")
                end
            end

            SetChosenDisplay(row, rowData)
            SetNameTierIcon(row, rowData)
            row.price:SetText(FormatMoneyText(rowData.pendingUnitPrice or rowData.minPrice))
            row.available:SetText(rowData.totalQuantity ~= nil and tostring(rowData.totalQuantity) or T("PG_DASH", "—"))
            row.status:SetText(rowData.status or T("PG_SHOP_SEARCH_RESULT", "Search result"))
            row.scanBtn:Hide()
            if row.deleteBtn then
                row.deleteBtn:Hide()
            end

            local buyEnabled = self.isAuctionHouseOpen and rowData.itemID ~= nil and displayQty > 0
            local buyAtlas = "Perks-ShoppingCart"
            local buyTitle = T("PG_SHOP_BUY", "Buy")
            local buyTip = T("PG_SHOP_BUY_TOOLTIP", "Start purchase for this item.")

            row.buyBtn.rowIndex = nil
            row.buyBtn.mode = "buy_search_result"
            row.buyBtn.searchRow = rowData
            row.buyBtn.itemID = rowData.itemID
            row.buyBtn.itemName = rowData.label

            if rowData.purchaseStage == "await_price" then
                buyEnabled = false
                buyTitle = T("PG_SHOP_WAIT", "Wait...")
                buyTip = T("PG_SHOP_WAIT_TOOLTIP", "Waiting for auction house price confirmation.")
            elseif rowData.purchaseStage == "confirm" then
                buyEnabled = true
                buyAtlas = "VAS-icon-checkmark"
                buyTitle = T("PG_SHOP_CONFIRM", "Confirm")
                buyTip = T("PG_SHOP_CONFIRM_TOOLTIP", "Confirm the current auction house price.")
            elseif rowData.purchaseStage == "processing" or rowData.searchPending or rowData.purchaseStage == "await_search" then
                buyEnabled = false
                buyTitle = rowData.purchaseStage == "processing" and T("PG_SHOP_STATUS_BUYING", "Buying...") or T("PG_SHOP_STATUS_SCANNING", "Scanning...")
                buyTip = ""
            elseif displayQty <= 0 then
                buyEnabled = false
                buyTitle = T("PG_SHOP_STATUS_BOUGHT", "Bought")
                buyTip = ""
            elseif rowData.totalQuantity ~= nil and tonumber(rowData.totalQuantity) <= 0 then
                buyEnabled = false
                buyTitle = T("PG_SHOP_STATUS_NO_ITEM", "No item")
                buyTip = ""
            elseif rowData.isCommodity == false and rowData.bestAuctionID == nil then
                buyEnabled = true
                buyTitle = T("PG_SHOP_BUY", "Buy")
                buyTip = T("PG_SHOP_BUY_TOOLTIP", "Start purchase for this item.")
            end

            if buyAtlas == "VAS-icon-checkmark" then
                row.buyBtn.iconSize = BUY_CONFIRM_ICON_SIZE
            else
                row.buyBtn.iconSize = BUY_ICON_SIZE
            end

            SetButtonAtlas(row.buyBtn, buyAtlas)
            row.buyBtn.tooltipTitle = buyTitle
            row.buyBtn.tooltipText = buyTip
            row.buyBtn:SetEnabled(buyEnabled)
            row.buyBtn:SetAlpha(buyEnabled and 1 or 0.30)
            row.buyBtn:Show()
        else
            local pendingLock = rowData.searchPending and rowData.chosenItemID == nil
            local canScan = self.isAuctionHouseOpen and remaining > 0 and not rowData.searchPending
            row.scanBtn:SetEnabled(canScan)
            row.scanBtn:SetAlpha(canScan and 0.75 or 0.25)
            row.scanBtn:Show()

            if row.deleteBtn then
                local canDelete = rowData.purchaseStage ~= "await_price" and rowData.purchaseStage ~= "processing"
                row.deleteBtn.rowIndex = rowData.index
                row.deleteBtn:SetEnabled(canDelete)
                row.deleteBtn:SetAlpha(canDelete and 0.85 or 0.25)
                row.deleteBtn:Show()
            end

            local buyEnabled = self.isAuctionHouseOpen and remaining > 0 and not pendingLock and rowData.chosenItemID ~= nil and rowData.minPrice ~= nil
            local buyAtlas = "Perks-ShoppingCart"
            local buyTitle = T("PG_SHOP_BUY", "Buy")
            local buyTip = T("PG_SHOP_BUY_TOOLTIP", "Start purchase for this item.")

            if rowData.purchaseStage == "await_price" then
                buyEnabled = false
                buyTitle = T("PG_SHOP_WAIT", "Wait...")
                buyTip = T("PG_SHOP_WAIT_TOOLTIP", "Waiting for auction house price confirmation.")
            elseif rowData.purchaseStage == "confirm" then
                buyEnabled = true
                buyAtlas = "VAS-icon-checkmark"
                buyTitle = T("PG_SHOP_CONFIRM", "Confirm")
                buyTip = T("PG_SHOP_CONFIRM_TOOLTIP", "Confirm the current auction house price.")
            elseif rowData.purchaseStage == "processing" then
                buyEnabled = false
                buyTitle = T("PG_SHOP_STATUS_BUYING", "Buying...")
                buyTip = ""
            elseif rowData.isCommodity == false and not rowData.bestAuctionID then
                buyEnabled = false
            elseif rowData.isCommodity == true and (not rowData.totalQuantity or rowData.totalQuantity <= 0) then
                buyEnabled = false
            end

            if buyAtlas == "VAS-icon-checkmark" then
                row.buyBtn.iconSize = BUY_CONFIRM_ICON_SIZE
            else
                row.buyBtn.iconSize = BUY_ICON_SIZE
            end

            SetButtonAtlas(row.buyBtn, buyAtlas)
            row.buyBtn.tooltipTitle = buyTitle
            row.buyBtn.tooltipText = buyTip
            row.buyBtn:SetEnabled(buyEnabled)
            row.buyBtn:SetAlpha(buyEnabled and 1 or 0.30)
            row.buyBtn:Show()
        end
    end

    for index = total + 1, #f.rows do
        local row = f.rows[index]
        row:Hide()
        row.rowIndex = nil
        row.scanBtn.rowIndex = nil
        row.buyBtn.rowIndex = nil
        row.qtyBox.rowIndex = nil
        row.qtyBox.searchRow = nil
        row.buyBtn.mode = nil
        row.buyBtn.itemID = nil
        row.scanBtn.itemID = nil
        row.searchRow = nil
        if row.name then
            row.name:SetTextColor(1, 1, 1)
        end
        if row.nameTierIcon then
            row.nameTierIcon:Hide()
        end
        if row.deleteBtn then
            row.deleteBtn.rowIndex = nil
        end
        if row.hotspot then
            row.hotspot.rowIndex = nil
            row.hotspot.itemID = nil
            row.hotspot.searchRow = nil
        end
    end

    local contentHeight = math.max(1, total * (ROW_HEIGHT + ROW_GAP))
    f.content:SetHeight(contentHeight)

    if f.scroll then
        C_Timer.After(0, function()
            if not f.scroll or not f.scroll:IsShown() then
                return
            end

            local child = f.scroll:GetScrollChild()
            local maxScroll = math.max(0, (child and child:GetHeight() or 0) - (f.scroll:GetHeight() or 0))
            local target = savedScroll or 0
            if target < 0 then target = 0 end
            if target > maxScroll then target = maxScroll end
            f.scroll:SetVerticalScroll(target)
        end)
    end

    local totalCost = 0
    local left = 0
    local done = 0

    for _, row in ipairs(allRows) do
        if row.hidden then
            done = done + 1
        else
            local remaining = GetRemainingQuantity(row)
            left = left + remaining
            if row.minPrice and remaining > 0 then
                totalCost = totalCost + (row.minPrice * remaining)
            end
        end
    end

    f.total:SetText(string.format(T("PG_SHOP_TOTAL", "Rows: %d  |  Closed: %d  |  Left: %d  |  Total: %s"), #allRows, done, left, FormatMoneyText(totalCost)))

    local hasRemaining = false
    for _, row in ipairs(allRows) do
        if GetRemainingQuantity(row) > 0 then
            hasRemaining = true
            break
        end
    end

    if self.searchResults and self.searchResults.active then
        if self.searchResults.pending then
            f.hint:SetText(T("PG_SHOP_SEARCHING", "Searching..."))
        elseif self.searchResults.lastError and #(self.searchResults.rows or {}) == 0 then
            f.hint:SetText(self.searchResults.lastError)
        else
            f.hint:SetText(string.format(T("PG_SHOP_SEARCH_RESULTS", "Results: %d"), #(self.searchResults.rows or {})))
        end
    elseif self.scanAllState.active then
        local doneCount = math.min(self.scanAllState.pointer or 0, #(self.scanAllState.queue or {}))
        local totalQueue = #(self.scanAllState.queue or {})
        f.hint:SetText(string.format(T("PG_SHOP_HINT_SCAN", "Scan: %d/%d"), doneCount, totalQueue))
    elseif self.buyAllState.active then
        local ptr = math.min(self.buyAllState.pointer or 1, #(self.buyAllState.order or {}))
        local totalOrder = #(self.buyAllState.order or {})
        f.hint:SetText(string.format(T("PG_SHOP_HINT_BUY", "Buying: %d/%d"), ptr, totalOrder))
    else
        f.hint:SetText(self.isAuctionHouseOpen and T("PG_SHOP_AH_OPEN", "AH open") or T("PG_SHOP_AH_CLOSED", "AH closed"))
    end

    if f.scanAll then f.scanAll:SetEnabled(self.isAuctionHouseOpen and hasRemaining) end
    if f.buyAll then f.buyAll:SetEnabled(false) end
    if f.refreshAll then f.refreshAll:SetEnabled(#allRows > 0) end
    if f.craftSimIntercept then
        f.craftSimIntercept:Show()
        if f.craftSimIntercept.Refresh then
            f.craftSimIntercept:Refresh()
        end
    end
    if f.searchOptionsButton then
        f.searchOptionsButton:Show()
        f.searchOptionsButton:SetEnabled(true)
        f.searchOptionsButton:SetAlpha(1)
    end
    if f.searchButton then
        f.searchButton:Show()
        f.searchButton:SetEnabled(true)
        f.searchButton:SetAlpha(1)
    end
    if f.addToListButton then
        f.addToListButton:Show()
        local canAdd = showingSearchResults and S.selectedSearchResult and S.selectedSearchResult.itemID ~= nil
        f.addToListButton:SetEnabled(canAdd)
        f.addToListButton:SetAlpha(canAdd and 1 or 0.45)
    end

    RefreshSearchClearButton(f)
end

SLASH_AHUISHOPPINGLIST1 = "/ahuishop"
SLASH_AHUISHOPPINGLIST2 = "/ahuishopping"
SlashCmdList["AHUISHOPPINGLIST"] = function()
    if not S.session or not S.session.active then
        if S.CreateManualList then
            S:CreateManualList(T("PG_SHOP_MANUAL_LIST_DEFAULT", "Manual list"))
        end
    else
        S:ShowWindow()
    end
end
