local PT = AhUI
if not PT then return end

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

local FONT = AhUI.FONT or STANDARD_TEXT_FONT
local L = PT.L or {}

local function T(key, default)
    return L[key] or default or key
end

local TITLE = "PG_SHOP_TITLE"
local ROW_HEIGHT = 28
local ROW_GAP = 2
local SEARCH_PENDING_TIMEOUT = 2.0
local WINDOW_WIDTH = 920
local WINDOW_HEIGHT = 420

local function GetDB()
    AhUI_DB = AhUI_DB or {}
    AhUI_DB.professionGuidesShopping = AhUI_DB.professionGuidesShopping or {}
    return AhUI_DB.professionGuidesShopping
end

local function SaveWindowPosition(frame)
    if not frame then return end
    local point, _, relPoint, x, y = frame:GetPoint(1)
    local db = GetDB()
    db.point = point or "CENTER"
    db.relPoint = relPoint or point or "CENTER"
    db.x = x or 280
    db.y = y or 0
end

local function RestoreWindowPosition(frame)
    local db = GetDB()
    frame:ClearAllPoints()
    frame:SetPoint(
        db.point or "CENTER",
        UIParent,
        db.relPoint or db.point or "CENTER",
        db.x or 280,
        db.y or 0
    )
end

S.session = {
    active = false,
    rows = {},
    skillLineID = nil,
    itemData = {},
}
S.isAuctionHouseOpen = false
S.scanAllState = {
    active = false,
    queue = nil,
    pointer = 0,
    token = 0,
}
S.buyAllState = {
    active = false,
    order = nil,
    pointer = 1,
    currentRowIndex = nil,
}
S.frame = S.frame or nil
S.pendingCommodityRow = nil
S.pendingItemPurchases = {}
S.OnShoppingDataChanged = S.OnShoppingDataChanged or nil
S.OnAuctionStateChanged = S.OnAuctionStateChanged or nil

local function IsAHAvailable()
    return C_AuctionHouse
        and C_AuctionHouse.MakeItemKey
        and C_AuctionHouse.SendSearchQuery
        and C_AuctionHouse.GetNumCommoditySearchResults
        and C_AuctionHouse.GetCommoditySearchResultInfo
        and C_AuctionHouse.GetNumItemSearchResults
        and C_AuctionHouse.GetItemSearchResultInfo
end

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

local function GetItemName(itemID)
    if not itemID then return "?" end
    local name = GetItemInfo(itemID)
    return name or ("item:" .. tostring(itemID))
end

local function GetItemIcon(itemID)
    local _, _, _, _, icon = GetItemInfoInstant(itemID)
    return icon or 134400
end

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

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

local function MarkRowPurchased(row, statusText)
    if not row then return end

    row.remainingQuantity = 0
    row.searchPending = false
    row.purchaseStage = nil
    row.pendingQuantity = nil
    row.pendingUnitPrice = nil
    row.pendingTotalPrice = nil

    row.totalQuantity = 0
    row.minPrice = nil
    row.bestAuctionID = nil
    row.bestAuctionQuantity = nil
    row.bestBuyoutAmount = nil

    row.hidden = true
    row.status = statusText or T("PG_SHOP_STATUS_BOUGHT", "Bought")
end

local function IsRowBuyableNow(row)
    if not row or GetRemainingQuantity(row) <= 0 or not row.chosenItemID or not row.minPrice then
        return false
    end

    if row.purchaseStage == "await_price" or row.purchaseStage == "confirm" or row.purchaseStage == "processing" or row.searchPending then
        return true
    end

    if row.isCommodity == true then
        return (tonumber(row.totalQuantity) or 0) > 0
    end

    if row.isCommodity == false then
        return row.bestAuctionID ~= nil and row.bestBuyoutAmount ~= nil
    end

    return false
end

local function CopyOptions(options)
    local out = {}
    for idx, opt in ipairs(options or {}) do
        if type(opt) == "table" and opt.itemID then
            table.insert(out, {
                itemID = opt.itemID,
                tier = opt.tier ~= nil and opt.tier or idx,
            })
        end
    end
    return out
end

local function GetOptionLabel(opt)
    if not opt or not opt.itemID then
        return T("PG_DASH", "—")
    end

    local name = GetItemName(opt.itemID)
    if opt.tier ~= nil then
        return string.format("%s |cff7f7f7f(R%d)|r", name, opt.tier)
    end
    return name
end

local function GetRowBaseLabel(row)
    if row.label and row.label ~= "" then
        return row.label
    end
    if row.itemID then
        return GetItemName(row.itemID)
    end
    if row.options and row.options[1] then
        return GetItemName(row.options[1].itemID)
    end
    return "?"
end

local function NormalizeMaterials(materials)
    local rows = {}
    if not materials then
        return rows
    end

    for _, entry in ipairs(materials) do
        if type(entry) == "table" and (entry.itemID or entry.options or entry.anyOf) then
            local row = {
                itemID = entry.itemID,
                options = CopyOptions(entry.options or entry.anyOf),
                quantity = tonumber(entry.quantity or entry.count) or 0,
                label = entry.label,
            }
            if row.quantity > 0 and (row.itemID or (#row.options > 0)) then
                table.insert(rows, row)
            end
        end
    end

    table.sort(rows, function(a, b)
        local an = GetRowBaseLabel(a)
        local bn = GetRowBaseLabel(b)
        if an == bn then
            local aid = a.itemID or (a.options[1] and a.options[1].itemID) or 0
            local bid = b.itemID or (b.options[1] and b.options[1].itemID) or 0
            return aid < bid
        end
        return an < bn
    end)

    for index, row in ipairs(rows) do
        row.index = index
        row.remainingQuantity = row.quantity
        row.status = T("PG_SHOP_STATUS_NOT_SCANNED", "Not scanned")
        row.searchPending = false
        row.purchaseStage = nil
        row.pendingQuantity = nil
        row.pendingUnitPrice = nil
        row.pendingTotalPrice = nil
        row.chosenItemID = row.itemID
        row.chosenTier = nil
        row.minPrice = nil
        row.totalQuantity = nil
        row.isCommodity = nil
        row.bestAuctionID = nil
        row.bestAuctionQuantity = nil
        row.bestBuyoutAmount = nil
        row.lastChosenName = nil
        row.hidden = false
    end

    return rows
end

local function EnsureItemInfoLoaded(rows, onReady)
    if not C_Item or not C_Item.RequestLoadItemDataByID then
        onReady()
        return
    end

    local pending = {}

    for _, row in ipairs(rows or {}) do
        if row.itemID then
            if not GetItemInfo(row.itemID) then
                pending[row.itemID] = true
                C_Item.RequestLoadItemDataByID(row.itemID)
            end
        end
        for _, opt in ipairs(row.options or {}) do
            if opt.itemID and not GetItemInfo(opt.itemID) then
                pending[opt.itemID] = true
                C_Item.RequestLoadItemDataByID(opt.itemID)
            end
        end
    end

    if not next(pending) then
        onReady()
        return
    end

    local tries = 0
    local function Poll()
        tries = tries + 1
        for itemID in pairs(pending) do
            if GetItemInfo(itemID) then
                pending[itemID] = nil
            end
        end

        if not next(pending) or tries >= 20 then
            onReady()
            return
        end

        C_Timer.After(0.1, Poll)
    end

    Poll()
end

local function BuildSorts()
    return {
        {
            sortOrder = Enum.AuctionHouseSortOrder.Price,
            reverseSort = false,
        }
    }
end

local function RefreshWindow()
    if not S.frame or not S.frame:IsShown() then
        return
    end
    if S.RefreshWindow then
        S:RefreshWindow()
    end
    if S.OnShoppingDataChanged then
        S:OnShoppingDataChanged()
    end
end

local function GetItemData(itemID)
    if not S.session.itemData[itemID] then
        S.session.itemData[itemID] = {
            itemID = itemID,
            itemKey = nil,
            searched = false,
            pending = false,
            pendingSince = nil,
            isCommodity = nil,
            minPrice = nil,
            totalQuantity = nil,
            bestAuctionID = nil,
            bestAuctionQuantity = nil,
            bestBuyoutAmount = nil,
        }
    end
    return S.session.itemData[itemID]
end

local function ClearStalePendingSearches(force)
    local now = GetTime and GetTime() or 0
    local changed = false

    for _, data in pairs(S.session.itemData or {}) do
        if data.pending then
            local stale = force
            if not stale and data.pendingSince then
                stale = (now - data.pendingSince) >= SEARCH_PENDING_TIMEOUT
            end
            if stale then
                data.pending = false
                data.pendingSince = nil
                if not data.searched then
                    data.searched = true
                end
                if data.totalQuantity == nil then
                    data.totalQuantity = 0
                end
                changed = true
            end
        end
    end

    return changed
end

local function FindRowByItemID(itemID)
    for _, row in ipairs(S.session.rows or {}) do
        if not row.hidden then
            if row.itemID == itemID then
                return row
            end
            for _, opt in ipairs(row.options or {}) do
                if opt.itemID == itemID then
                    return row
                end
            end
        end
    end
    return nil
end

local function FindRowByAuctionID(auctionID)
    for _, pending in pairs(S.pendingItemPurchases or {}) do
        if pending.auctionID == auctionID and pending.rowIndex then
            return S.session.rows and S.session.rows[pending.rowIndex], pending
        end
    end
    return nil, nil
end

local function ResolveRow(row)
    if not row then return end

    if GetRemainingQuantity(row) <= 0 then
        row.status = T("PG_SHOP_STATUS_BOUGHT", "Bought")
        row.searchPending = false
        return
    end

    if row.options and #row.options > 0 then
        local bestData, bestOpt
        local anySearched = false
        local anyPending = false

        for _, opt in ipairs(row.options) do
            local data = GetItemData(opt.itemID)
            if data.pending then
                anyPending = true
            end
            if data.searched then
                anySearched = true
            end
            if data.minPrice and data.minPrice > 0 then
                if (not bestData) or data.minPrice < bestData.minPrice then
                    bestData = data
                    bestOpt = opt
                end
            end
        end

        row.searchPending = anyPending

        if bestData and bestOpt then
            row.chosenItemID = bestOpt.itemID
            row.chosenTier = bestOpt.tier
            row.minPrice = bestData.minPrice
            row.totalQuantity = bestData.totalQuantity
            row.isCommodity = bestData.isCommodity
            row.bestAuctionID = bestData.bestAuctionID
            row.bestAuctionQuantity = bestData.bestAuctionQuantity
            row.bestBuyoutAmount = bestData.bestBuyoutAmount
            row.lastChosenName = GetOptionLabel(bestOpt)
            row.status = bestData.totalQuantity and bestData.totalQuantity > 0 and T("PG_SHOP_STATUS_SELECTED", "Selected") or T("PG_SHOP_STATUS_NO_ITEM", "No item")
        else
            row.chosenItemID = nil
            row.chosenTier = nil
            row.minPrice = nil
            row.totalQuantity = nil
            row.isCommodity = nil
            row.bestAuctionID = nil
            row.bestAuctionQuantity = nil
            row.bestBuyoutAmount = nil
            row.lastChosenName = nil
            if anyPending then
                row.status = T("PG_SHOP_STATUS_SCANNING", "Scanning...")
            elseif anySearched then
                row.status = T("PG_SHOP_STATUS_NO_PRICE", "No price")
            else
                row.status = T("PG_SHOP_STATUS_NOT_SCANNED", "Not scanned")
            end
        end
    else
        local data = GetItemData(row.itemID)
        row.searchPending = data.pending
        row.chosenItemID = row.itemID
        row.chosenTier = nil
        row.minPrice = data.minPrice
        row.totalQuantity = data.totalQuantity
        row.isCommodity = data.isCommodity
        row.bestAuctionID = data.bestAuctionID
        row.bestAuctionQuantity = data.bestAuctionQuantity
        row.bestBuyoutAmount = data.bestBuyoutAmount
        row.lastChosenName = GetItemName(row.itemID)
        if data.pending then
            row.status = T("PG_SHOP_STATUS_SCANNING", "Scanning...")
        elseif data.searched then
            row.status = data.totalQuantity and data.totalQuantity > 0 and T("PG_SHOP_STATUS_FOUND", "Found") or T("PG_SHOP_STATUS_NO_ITEM", "No item")
        else
            row.status = T("PG_SHOP_STATUS_NOT_SCANNED", "Not scanned")
        end
    end
end

local function ResolveAllRows()
    for _, row in ipairs(S.session.rows or {}) do
        ResolveRow(row)
    end
    RefreshWindow()
end

local function QueueRefreshChosenSearch(row)
    if not row or not row.index or not row.chosenItemID then
        return
    end

    C_Timer.After(0.35, function()
        if not S.session.active or not S.isAuctionHouseOpen then
            return
        end
        local liveRow = S.session.rows and S.session.rows[row.index]
        if not liveRow or GetRemainingQuantity(liveRow) <= 0 or not liveRow.chosenItemID then
            return
        end
        S:SearchItemID(liveRow.chosenItemID, true)
    end)
end

local function ApplyCommodityResults(itemID)
    local data = GetItemData(itemID)
    data.pending = false
    data.pendingSince = nil
    data.searched = true
    data.isCommodity = true
    data.bestAuctionID = nil
    data.bestAuctionQuantity = nil
    data.bestBuyoutAmount = nil

    local count = C_AuctionHouse.GetNumCommoditySearchResults(itemID) or 0
    if count <= 0 then
        data.minPrice = nil
        data.totalQuantity = 0
        ResolveAllRows()
        return
    end

    local minPrice = nil
    local totalQuantity = 0

    for index = 1, count do
        local ok, result = pcall(C_AuctionHouse.GetCommoditySearchResultInfo, itemID, index)
        if ok and type(result) == "table" then
            if result.unitPrice and result.unitPrice > 0 then
                if not minPrice or result.unitPrice < minPrice then
                    minPrice = result.unitPrice
                end
            end
            if result.quantity then
                totalQuantity = totalQuantity + result.quantity
            end
        end
    end

    data.minPrice = minPrice
    data.totalQuantity = totalQuantity
    ResolveAllRows()
end

local function ApplyItemResults(itemKey)
    local itemID = itemKey and itemKey.itemID
    if not itemID then
        return
    end

    local data = GetItemData(itemID)
    data.pending = false
    data.pendingSince = nil
    data.searched = true
    data.isCommodity = false

    local count = C_AuctionHouse.GetNumItemSearchResults(itemKey) or 0
    if count <= 0 then
        data.minPrice = nil
        data.totalQuantity = 0
        data.bestAuctionID = nil
        data.bestAuctionQuantity = nil
        data.bestBuyoutAmount = nil
        ResolveAllRows()
        return
    end

    local minPrice = nil
    local totalQuantity = 0
    local bestAuctionID = nil
    local bestAuctionQuantity = nil
    local bestBuyoutAmount = nil

    for index = 1, count do
        local ok, result = pcall(C_AuctionHouse.GetItemSearchResultInfo, itemKey, index)
        if ok and type(result) == "table" then
            local listingPrice = nil
            if result.buyoutAmount and result.buyoutAmount > 0 then
                listingPrice = result.buyoutAmount
            elseif result.bidAmount and result.bidAmount > 0 then
                listingPrice = result.bidAmount
            end

            if listingPrice and (not minPrice or listingPrice < minPrice) then
                minPrice = listingPrice
                bestAuctionID = result.auctionID
                bestAuctionQuantity = result.quantity or 1
                bestBuyoutAmount = result.buyoutAmount or result.bidAmount
            end

            totalQuantity = totalQuantity + (result.quantity or 1)
        end
    end

    data.minPrice = minPrice
    data.totalQuantity = totalQuantity
    data.bestAuctionID = bestAuctionID
    data.bestAuctionQuantity = bestAuctionQuantity
    data.bestBuyoutAmount = bestBuyoutAmount
    ResolveAllRows()
end

local function CreateRow(parent, index)
    local row = CreateFrame("Frame", nil, parent)
    row:SetHeight(ROW_HEIGHT)

    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 = row:CreateFontString(nil, "OVERLAY")
    row.name:SetFont(FONT, 11)
    row.name:SetPoint("LEFT", row.icon, "RIGHT", 8, 0)
    row.name:SetJustifyH("LEFT")
    row.name:SetWidth(180)

    row.need = row:CreateFontString(nil, "OVERLAY")
    row.need:SetFont(FONT, 11)
    row.need:SetPoint("LEFT", 210, 0)
    row.need:SetWidth(70)
    row.need:SetJustifyH("RIGHT")

    row.chosen = row:CreateFontString(nil, "OVERLAY")
    row.chosen:SetFont(FONT, 11)
    row.chosen:SetPoint("LEFT", 292, 0)
    row.chosen:SetWidth(220)
    row.chosen:SetJustifyH("LEFT")

    row.price = row:CreateFontString(nil, "OVERLAY")
    row.price:SetFont(FONT, 11)
    row.price:SetPoint("LEFT", 522, 0)
    row.price:SetWidth(80)
    row.price:SetJustifyH("LEFT")

    row.available = row:CreateFontString(nil, "OVERLAY")
    row.available:SetFont(FONT, 11)
    row.available:SetPoint("LEFT", 612, 0)
    row.available:SetWidth(55)
    row.available:SetJustifyH("RIGHT")

    row.status = row:CreateFontString(nil, "OVERLAY")
    row.status:SetFont(FONT, 10)
    row.status:SetPoint("LEFT", 678, 0)
    row.status:SetWidth(95)
    row.status:SetJustifyH("LEFT")

    row.scanBtn = CreateFrame("Button", nil, row, "UIPanelButtonTemplate")
    row.scanBtn:SetSize(58, 20)
    row.scanBtn:SetPoint("RIGHT", -72, 0)
    row.scanBtn:SetText(T("PG_SHOP_SCAN", "Scan"))
    row.scanBtn:SetScript("OnClick", function(self)
        if self.rowIndex then
            S:ScanRow(self.rowIndex)
        end
    end)

    row.buyBtn = CreateFrame("Button", nil, row, "UIPanelButtonTemplate")
    row.buyBtn:SetSize(58, 20)
    row.buyBtn:SetPoint("RIGHT", -8, 0)
    row.buyBtn:SetText(T("PG_SHOP_BUY", "Buy"))
    row.buyBtn:SetScript("OnClick", function(self)
        if self.rowIndex then
            S:BuyRow(self.rowIndex)
        end
    end)

    row.doneIcon = row:CreateTexture(nil, "OVERLAY")
    row.doneIcon:SetSize(16, 16)
    row.doneIcon:SetPoint("CENTER", row.buyBtn, "CENTER", 0, 0)
    row.doneIcon:SetAtlas("common-icon-checkmark")
    row.doneIcon:SetVertexColor(0.2, 1, 0.2)
    row.doneIcon:Hide()

    return row
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 f = CreateFrame("Frame", "AhUIProfessionGuideShopping", UIParent, "BackdropTemplate")
    table.insert(UISpecialFrames, f:GetName())
    f:SetSize(WINDOW_WIDTH, WINDOW_HEIGHT)
    RestoreWindowPosition(f)
    f:SetFrameStrata("HIGH")
    f:SetMovable(true)
    f:EnableMouse(true)
    f:RegisterForDrag("LeftButton")
    f:SetScript("OnDragStart", function(self)
        self:StartMoving()
    end)

    f:SetScript("OnDragStop", function(self)
        self:StopMovingOrSizing()
        SaveWindowPosition(self)
    end)
    f:SetClampedToScreen(true)
    f:Hide()
    f:SetScript("OnHide", function()
        S:StopScanAll()
        S:StopBuyAll()
    end)

    f:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8x8",
        edgeFile = "Interface\\Buttons\\WHITE8x8",
        edgeSize = 1,
    })
    f:SetBackdropColor(0.05, 0.05, 0.07, 0.96)
    f:SetBackdropBorderColor(1, 1, 1, 0.12)

    f.title = f:CreateFontString(nil, "OVERLAY")
    f.title:SetFont(FONT, 13)
    f.title:SetPoint("TOPLEFT", 12, -10)
    f.title:SetText(T(TITLE, "AhUI: shopping list"))

    f.subtitle = f:CreateFontString(nil, "OVERLAY")
    f.subtitle:SetFont(FONT, 11)
    f.subtitle:SetPoint("TOPLEFT", f.title, "BOTTOMLEFT", 0, -4)
    f.subtitle:SetText(T("PG_SHOP_SUBTITLE", "Scan all grades and buy from the AhUI window"))

    f.close = CreateFrame("Button", nil, f, "UIPanelCloseButton")
    f.close:SetPoint("TOPRIGHT", -4, -4)

    f.hint = f:CreateFontString(nil, "OVERLAY")
    f.hint:SetFont(FONT, 10)
    f.hint:SetPoint("TOPRIGHT", -36, -16)
    f.hint:SetText("")

    f.scanAll = CreateFrame("Button", nil, f, "UIPanelButtonTemplate")
    f.scanAll:SetSize(130, 20)
    f.scanAll:SetPoint("TOPLEFT", 12, -42)
    f.scanAll:SetText(T("PG_SHOP_SCAN_ALL", "Scan all"))
    f.scanAll:SetScript("OnClick", function()
        S:ScanAllRows()
    end)

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

    f.refreshAll = CreateFrame("Button", nil, f, "UIPanelButtonTemplate")
    f.refreshAll:SetSize(100, 20)
    f.refreshAll:SetPoint("LEFT", f.scanAll, "RIGHT", 8, 0)
    f.refreshAll:SetText(T("PG_SHOP_RESET_PRICES", "Reset prices"))
    f.refreshAll:SetScript("OnClick", function()
        S:ResetPrices()
    end)

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

    local function MakeHeader(text, x, w, justify)
        local fs = f.header:CreateFontString(nil, "OVERLAY")
        fs:SetFont(FONT, 10)
        fs:SetPoint("LEFT", x, 0)
        fs:SetWidth(w)
        fs:SetJustifyH(justify or "LEFT")
        fs:SetText(text)
        return fs
    end

    MakeHeader(T("PG_SHOP_COL_ITEM", "Item"), 10, 180)
    MakeHeader(T("PG_SHOP_COL_LEFT", "Left"), 205, 70, "RIGHT")
    MakeHeader(T("PG_SHOP_COL_SELECTED", "Selected"), 290, 220)
    MakeHeader(T("PG_SHOP_COL_PRICE", "Price"), 522, 80)
    MakeHeader(T("PG_SHOP_COL_AH", "On AH"), 612, 55, "RIGHT")
    MakeHeader(T("PG_SHOP_COL_STATUS", "Status"), 678, 95)

    f.scroll = CreateFrame("ScrollFrame", nil, f, "UIPanelScrollFrameTemplate")
    f.scroll:SetPoint("TOPLEFT", f.header, "BOTTOMLEFT", 0, -4)
    f.scroll:SetPoint("BOTTOMRIGHT", -30, 36)
    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 maxScroll = math.max(0, (self:GetScrollChild():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(WINDOW_WIDTH - 60, 1)
    f.scroll:SetScrollChild(f.content)
    f.rows = {}

    f.empty = f:CreateFontString(nil, "OVERLAY")
    f.empty:SetFont(FONT, 11)
    f.empty:SetPoint("CENTER", 0, -12)
    f.empty:SetText(T("PG_SHOP_EMPTY", "List is empty"))
    f.empty:Hide()

    f.total = f:CreateFontString(nil, "OVERLAY")
    f.total:SetFont(FONT, 11)
    f.total:SetPoint("BOTTOMLEFT", 12, 12)
    f.total:SetText("")

    self.frame = f
    return f
end

function S:IsAvailable()
    return IsAHAvailable()
end

function S:CanBuy(skillLineID)
    return skillLineID ~= nil and IsAHAvailable()
end

function S:StopScanAll()
    self.scanAllState.active = false
    self.scanAllState.queue = nil
    self.scanAllState.pointer = 0
    self.scanAllState.token = (self.scanAllState.token or 0) + 1
    if ClearStalePendingSearches(false) then
        ResolveAllRows()
        return
    end
    RefreshWindow()
end

function S:StopBuyAll()
    self.buyAllState.active = false
    self.buyAllState.order = nil
    self.buyAllState.pointer = 1
    self.buyAllState.currentRowIndex = nil
    RefreshWindow()
end

function S:EndShoppingSession()
    self:StopScanAll()
    self:StopBuyAll()
    self.session = {
        active = false,
        rows = {},
        skillLineID = nil,
        itemData = {},
    }
    self.pendingCommodityRow = nil
    self.pendingItemPurchases = {}
    if self.frame then
        self.frame:Hide()
    end
end

function S:GetRows()
    return self.session and self.session.rows or {}
end

function S:ShowWindow()
    local f = self:CreateWindow()
    f:Show()
    if f.scroll then
        f.scroll:SetVerticalScroll(0)
    end
    self:RefreshWindow()
end

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

function S:ResetPrices()
    self:StopScanAll()
    self:StopBuyAll()

    if self.pendingCommodityRow and C_AuctionHouse and C_AuctionHouse.CancelCommoditiesPurchase then
        pcall(C_AuctionHouse.CancelCommoditiesPurchase)
    end

    self.pendingCommodityRow = nil
    self.pendingItemPurchases = {}
    self.session.itemData = {}

    for _, row in ipairs(self:GetRows()) do
        row.hidden = false
        row.remainingQuantity = row.quantity or 0
        row.status = T("PG_SHOP_STATUS_NOT_SCANNED", "Not scanned")
        row.searchPending = false
        row.purchaseStage = nil
        row.pendingQuantity = nil
        row.pendingUnitPrice = nil
        row.pendingTotalPrice = nil
        row.chosenItemID = row.itemID
        row.chosenTier = nil
        row.minPrice = nil
        row.totalQuantity = nil
        row.isCommodity = nil
        row.bestAuctionID = nil
        row.bestAuctionQuantity = nil
        row.bestBuyoutAmount = nil
        row.lastChosenName = nil
    end

    RefreshWindow()
end

function S:CreateShoppingSession(materials, skillLineID)
    if not IsAHAvailable() then
        return false, "no_auction_house_api"
    end

    local rows = NormalizeMaterials(materials)
    if not rows or #rows == 0 then
        return false, "empty"
    end

    EnsureItemInfoLoaded(rows, function()
        self:StopScanAll()
        self:StopBuyAll()
        self.session.active = true
        self.session.rows = rows
        self.session.skillLineID = skillLineID
        self.session.itemData = {}
        self.pendingCommodityRow = nil
        self.pendingItemPurchases = {}
        self:ShowWindow()
    end)

    return true
end

function S:BuildScanQueue()
    local seen = {}
    local queue = {}

    for _, row in ipairs(self:GetRows()) do
        if GetRemainingQuantity(row) > 0 then
            if row.itemID then
                if not seen[row.itemID] then
                    seen[row.itemID] = true
                    table.insert(queue, row.itemID)
                end
            end
            for _, opt in ipairs(row.options or {}) do
                if opt.itemID and not seen[opt.itemID] then
                    seen[opt.itemID] = true
                    table.insert(queue, opt.itemID)
                end
            end
        end
    end

    table.sort(queue)
    return queue
end

function S:RunScanAllStep(token)
    if not self.scanAllState.active then
        return
    end
    if token ~= self.scanAllState.token then
        return
    end
    if not self.session.active or not self.isAuctionHouseOpen then
        self:StopScanAll()
        return
    end

    self.scanAllState.pointer = (self.scanAllState.pointer or 0) + 1
    local itemID = self.scanAllState.queue and self.scanAllState.queue[self.scanAllState.pointer]
    if not itemID then
        self:StopScanAll()
        if ClearStalePendingSearches(false) then
            ResolveAllRows()
        end
        return
    end

    self:SearchItemID(itemID, true)

    if self.scanAllState.active and token == self.scanAllState.token then
        C_Timer.After(0.35, function()
            S:RunScanAllStep(token)
        end)
    end
end

function S:ScanAllRows()
    if not self.session.active then
        return
    end
    if not self.isAuctionHouseOpen then
        RefreshWindow()
        return
    end

    if self.scanAllState.active then
        self:StopScanAll()
        return
    end

    self:StopBuyAll()

    local queue = self:BuildScanQueue()
    if #queue == 0 then
        RefreshWindow()
        return
    end

    self.scanAllState.active = true
    self.scanAllState.queue = queue
    self.scanAllState.pointer = 0
    self.scanAllState.token = (self.scanAllState.token or 0) + 1

    for _, row in ipairs(self:GetRows()) do
        if GetRemainingQuantity(row) > 0 then
            row.status = T("PG_SHOP_STATUS_QUEUED", "Queued")
            row.searchPending = false
        end
    end

    RefreshWindow()
    self:RunScanAllStep(self.scanAllState.token)
end

function S:ScanRow(index)
    self:StopScanAll()
    self:StopBuyAll()

    local row = self.session.rows and self.session.rows[index]
    if not row then
        return
    end

    if row.itemID then
        self:SearchItemID(row.itemID)
    end
    for _, opt in ipairs(row.options or {}) do
        self:SearchItemID(opt.itemID, true)
    end
end

function S:SearchItemID(itemID, silent)
    if not itemID then
        return
    end

    local data = GetItemData(itemID)
    if not self.isAuctionHouseOpen then
        data.pending = false
        data.pendingSince = nil
        data.searched = false
        ResolveAllRows()
        return
    end

    local ok, itemKey = pcall(C_AuctionHouse.MakeItemKey, itemID)
    if not ok or not itemKey then
        data.pending = false
        data.pendingSince = nil
        data.searched = false
        ResolveAllRows()
        return
    end

    data.itemKey = itemKey
    data.pending = true
    data.pendingSince = GetTime and GetTime() or 0
    data.searched = false
    data.isCommodity = nil
    data.minPrice = nil
    data.totalQuantity = nil
    data.bestAuctionID = nil
    data.bestAuctionQuantity = nil
    data.bestBuyoutAmount = nil

    if not silent then
        local row = FindRowByItemID(itemID)
        if row then
            row.status = T("PG_SHOP_STATUS_SCANNING", "Scanning...")
        end
    end

    ResolveAllRows()

    local okQuery, err = pcall(C_AuctionHouse.SendSearchQuery, itemKey, BuildSorts(), true)
    if okQuery then
        C_Timer.After(SEARCH_PENDING_TIMEOUT, function()
            if not S.session.active then
                return
            end
            local live = S.session.itemData and S.session.itemData[itemID]
            if live and live.pending then
                live.pending = false
                live.pendingSince = nil
                if not live.searched then
                    live.searched = true
                    if live.totalQuantity == nil then
                        live.totalQuantity = 0
                    end
                end
                ResolveAllRows()
            end
        end)
        return
    end

    data.pending = false
    data.pendingSince = nil
    data.searched = false
    local row = FindRowByItemID(itemID)
    if row then
        row.status = T("PG_SHOP_STATUS_SEARCH_ERROR", "Search error")
    end
    if err then
        print(T("PG_SHOP_ERR_SEARCH", "AhUI: auction search error:"), err)
    end
    ResolveAllRows()
end

function S:BeginCommodityPurchase(row)
    if not row or not row.chosenItemID or GetRemainingQuantity(row) <= 0 then
        return
    end
    if not self.isAuctionHouseOpen then
        row.status = T("PG_SHOP_STATUS_OPEN_AH", "Open AH")
        RefreshWindow()
        return
    end

    local available = tonumber(row.totalQuantity) or 0
    if available <= 0 then
        row.status = T("PG_SHOP_STATUS_NO_ITEM", "No item")
        RefreshWindow()
        return
    end

    local quantity = math.min(GetRemainingQuantity(row), available)
    if quantity <= 0 then
        row.status = T("PG_SHOP_STATUS_NO_ITEM", "No item")
        RefreshWindow()
        return
    end

    if self.pendingCommodityRow and self.pendingCommodityRow ~= row and C_AuctionHouse.CancelCommoditiesPurchase then
        pcall(C_AuctionHouse.CancelCommoditiesPurchase)
        self.pendingCommodityRow.purchaseStage = nil
        self.pendingCommodityRow.pendingQuantity = nil
        self.pendingCommodityRow.pendingUnitPrice = nil
        self.pendingCommodityRow.pendingTotalPrice = nil
    end

    self.pendingCommodityRow = row
    row.pendingQuantity = quantity
    row.pendingUnitPrice = nil
    row.pendingTotalPrice = nil
    row.purchaseStage = "await_price"
    row.status = T("PG_SHOP_STATUS_REQUESTING_PRICE", "Requesting price")
    RefreshWindow()

    local ok, err = pcall(C_AuctionHouse.StartCommoditiesPurchase, row.chosenItemID, quantity)
    if not ok then
        row.purchaseStage = nil
        row.pendingQuantity = nil
        row.status = T("PG_SHOP_STATUS_BUY_ERROR", "Purchase error")
        self.pendingCommodityRow = nil
        if err then
            print(T("PG_SHOP_ERR_START_BUY", "AhUI: start purchase error:"), err)
        end
        RefreshWindow()
    end
end

function S:ConfirmCommodityPurchase(row)
    if not row or row.purchaseStage ~= "confirm" or not row.pendingQuantity or row.pendingQuantity <= 0 then
        return
    end

    row.purchaseStage = "processing"
    row.status = T("PG_SHOP_STATUS_BUYING", "Buying...")
    RefreshWindow()

    local ok, err = pcall(C_AuctionHouse.ConfirmCommoditiesPurchase, row.chosenItemID, row.pendingQuantity)
    if not ok then
        row.purchaseStage = nil
        row.status = T("PG_SHOP_STATUS_CONFIRM_ERROR", "Confirmation error")
        if err then
            print(T("PG_SHOP_ERR_CONFIRM_BUY", "AhUI: purchase confirmation error:"), err)
        end
        RefreshWindow()
    end
end

function S:BuyItemAuction(row)
    if not row or GetRemainingQuantity(row) <= 0 then
        return
    end
    if not row.bestAuctionID or not row.bestBuyoutAmount then
        row.status = T("PG_SHOP_STATUS_NO_BUYOUT", "No buyout")
        RefreshWindow()
        return
    end

    local purchaseQuantity = row.bestAuctionQuantity or 1
    row.purchaseStage = "processing"
    row.status = T("PG_SHOP_STATUS_BUYING_AUCTION", "Buying auction")
    self.pendingItemPurchases[row.bestAuctionID] = {
        auctionID = row.bestAuctionID,
        rowIndex = row.index,
        quantity = purchaseQuantity,
        itemID = row.chosenItemID,
    }
    RefreshWindow()

    local ok, err = pcall(C_AuctionHouse.PlaceBid, row.bestAuctionID, row.bestBuyoutAmount)
    if not ok then
        self.pendingItemPurchases[row.bestAuctionID] = nil
        row.purchaseStage = nil
        row.status = T("PG_SHOP_STATUS_BUY_ERROR", "Purchase error")
        if err then
            print(T("PG_SHOP_ERR_BUY_AUCTION", "AhUI: auction purchase error:"), err)
        end
        RefreshWindow()
    end
end

function S:BuyRow(index)
    self:StopScanAll()

    local row = self.session.rows and self.session.rows[index]
    if not row then
        return
    end

    if GetRemainingQuantity(row) <= 0 then
        row.status = T("PG_SHOP_STATUS_BOUGHT", "Bought")
        RefreshWindow()
        return
    end

    if not self.isAuctionHouseOpen then
        row.status = T("PG_SHOP_STATUS_OPEN_AH", "Open AH")
        RefreshWindow()
        return
    end

    if row.searchPending then
        row.status = T("PG_SHOP_STATUS_WAIT_SCAN", "Wait for scan")
        RefreshWindow()
        return
    end

    if row.purchaseStage == "confirm" then
        self:ConfirmCommodityPurchase(row)
        return
    end

    if not row.chosenItemID then
        row.status = T("PG_SHOP_STATUS_SCAN_FIRST", "Scan first")
        RefreshWindow()
        return
    end

    if row.isCommodity == true then
        self:BeginCommodityPurchase(row)
        return
    end

    if row.bestAuctionID and row.bestBuyoutAmount then
        self:BuyItemAuction(row)
        return
    end

    self:SearchItemID(row.chosenItemID)
end

function S:BuildBuyAllOrder()
    local order = {}
    for index, row in ipairs(self:GetRows()) do
        if IsRowBuyableNow(row) then
            table.insert(order, index)
        end
    end
    return order
end

function S:HandleBuyAllClick()
    self:StopBuyAll()
    RefreshWindow()
end

function S:AdvanceBuyAll()
    if not self.buyAllState.active then
        return
    end
    if not self.isAuctionHouseOpen then
        self:StopBuyAll()
        return
    end

    local rows = self:GetRows()
    local currentIndex = self.buyAllState.currentRowIndex
    local currentRow = currentIndex and rows[currentIndex] or nil

    if currentRow then
        if currentRow.purchaseStage == "confirm" then
            self:ConfirmCommodityPurchase(currentRow)
            return
        end

        if currentRow.purchaseStage == "await_price" or currentRow.purchaseStage == "processing" or currentRow.searchPending then
            RefreshWindow()
            return
        end

        if GetRemainingQuantity(currentRow) <= 0 or not IsRowBuyableNow(currentRow) then
            self.buyAllState.pointer = (self.buyAllState.pointer or 1) + 1
            self.buyAllState.currentRowIndex = nil
            currentRow = nil
        elseif currentRow.chosenItemID and currentRow.minPrice then
            self:BuyRow(currentIndex)
            if currentRow.purchaseStage == "confirm" or currentRow.purchaseStage == "await_price" or currentRow.purchaseStage == "processing" or currentRow.searchPending then
                return
            end
            if GetRemainingQuantity(currentRow) <= 0 or not IsRowBuyableNow(currentRow) then
                self.buyAllState.pointer = (self.buyAllState.pointer or 1) + 1
                self.buyAllState.currentRowIndex = nil
            end
            RefreshWindow()
            return
        else
            self.buyAllState.pointer = (self.buyAllState.pointer or 1) + 1
            self.buyAllState.currentRowIndex = nil
        end
    end

    while self.buyAllState.active do
        local idx = self.buyAllState.order and self.buyAllState.order[self.buyAllState.pointer]
        if not idx then
            self:StopBuyAll()
            return
        end

        local row = rows[idx]
        if row and IsRowBuyableNow(row) then
            self.buyAllState.currentRowIndex = idx
            self:BuyRow(idx)
            return
        end

        self.buyAllState.pointer = (self.buyAllState.pointer or 1) + 1
    end
end

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

    local allRows = self:GetRows()
    local rows = GetVisibleRows()
    local total = #rows
    f.empty:SetShown(total == 0)

    EnsureVisibleRows(f, total)

    for index, rowData in ipairs(rows) do
        local row = f.rows[index]
        row:Show()
        row.scanBtn.rowIndex = rowData.index
        row.buyBtn.rowIndex = rowData.index

        local iconID = rowData.chosenItemID or rowData.itemID or (rowData.options[1] and rowData.options[1].itemID)
        row.icon:SetTexture(GetItemIcon(iconID))
        row.name:SetText(GetRowBaseLabel(rowData))

        local remaining = GetRemainingQuantity(rowData)
        if remaining ~= (rowData.quantity or 0) then
            row.need:SetText(string.format("%d/%d", remaining, rowData.quantity or 0))
        else
            row.need:SetText(tostring(remaining))
        end

        row.chosen:SetText(rowData.lastChosenName or (rowData.options and #rowData.options > 0 and T("PG_SHOP_AFTER_SCAN", "after scan") or GetItemName(rowData.itemID)))
        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)

        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:SetText(pendingLock and "..." or T("PG_SHOP_SCAN", "Scan"))
        row.scanBtn:Show()

        local buyText = T("PG_SHOP_BUY", "Buy")
        local buyEnabled = self.isAuctionHouseOpen and remaining > 0 and not pendingLock and rowData.chosenItemID ~= nil and rowData.minPrice ~= nil

        if rowData.purchaseStage == "await_price" then
            buyText = T("PG_SHOP_WAIT", "Wait...")
            buyEnabled = false
        elseif rowData.purchaseStage == "confirm" then
            buyText = T("PG_SHOP_CONFIRM", "Confirm")
            buyEnabled = true
        elseif rowData.purchaseStage == "processing" then
            buyText = "..."
            buyEnabled = false
        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

        row.buyBtn:SetText(buyText)
        row.buyBtn:SetEnabled(buyEnabled)
        row.buyBtn:Show()

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

    for index = total + 1, #f.rows do
        local row = f.rows[index]
        row:Hide()
        row.scanBtn.rowIndex = nil
        row.buyBtn.rowIndex = nil
        if row.doneIcon then
            row.doneIcon:Hide()
        end
    end

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

    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(rows) do
        if GetRemainingQuantity(row) > 0 then
            hasRemaining = true
            break
        end
    end

    if 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))
        f.scanAll:SetText(T("PG_SHOP_STOP", "Stop"))
    elseif self.buyAllState.active then
        local ptr = math.min(self.buyAllState.pointer or 1, #(self.buyAllState.order or {}))
        local totalOrder = #(self.buyAllState.order or {})
        local current = self.buyAllState.currentRowIndex and self.session.rows[self.buyAllState.currentRowIndex] or nil
        if current and current.purchaseStage == "confirm" then
            if f.buyAll then f.buyAll:SetText(T("PG_SHOP_CONFIRM_ALL", "Confirm all")) end
        else
            if f.buyAll then f.buyAll:SetText(T("PG_SHOP_BUY_ALL", "Buy all")) end
        end
        f.hint:SetText(string.format(T("PG_SHOP_HINT_BUY", "Buying: %d/%d"), ptr, totalOrder))
        f.scanAll:SetText(T("PG_SHOP_SCAN_ALL", "Scan all"))
    else
        f.hint:SetText(self.isAuctionHouseOpen and T("PG_SHOP_AH_OPEN", "AH open") or T("PG_SHOP_AH_CLOSED", "AH closed"))
        f.scanAll:SetText(T("PG_SHOP_SCAN_ALL", "Scan all"))
        if f.buyAll then f.buyAll:SetText(T("PG_SHOP_BUY_ALL", "Buy all")) end
    end

    f.scanAll:SetEnabled(self.isAuctionHouseOpen and hasRemaining)
    if f.buyAll then f.buyAll:SetEnabled(false) end
    f.refreshAll:SetEnabled(#allRows > 0)
end

local eventFrame = CreateFrame("Frame")
eventFrame:RegisterEvent("AUCTION_HOUSE_SHOW")
eventFrame:RegisterEvent("AUCTION_HOUSE_CLOSED")
eventFrame:RegisterEvent("COMMODITY_SEARCH_RESULTS_UPDATED")
eventFrame:RegisterEvent("ITEM_SEARCH_RESULTS_UPDATED")
eventFrame:RegisterEvent("COMMODITY_PRICE_UPDATED")
eventFrame:RegisterEvent("COMMODITY_PRICE_UNAVAILABLE")
eventFrame:RegisterEvent("COMMODITY_PURCHASED")
eventFrame:RegisterEvent("COMMODITY_PURCHASE_SUCCEEDED")
eventFrame:RegisterEvent("COMMODITY_PURCHASE_FAILED")
eventFrame:RegisterEvent("AUCTION_HOUSE_PURCHASE_COMPLETED")

eventFrame:SetScript("OnEvent", function(_, event, ...)
    if event == "AUCTION_HOUSE_SHOW" then
        S.isAuctionHouseOpen = true
        if S.OnAuctionStateChanged then
            S:OnAuctionStateChanged(true)
        end
        RefreshWindow()
        return
    end

    if event == "AUCTION_HOUSE_CLOSED" then
        S.isAuctionHouseOpen = false
        S:StopScanAll()
        S:StopBuyAll()
        S.pendingCommodityRow = nil
        S.pendingItemPurchases = {}
        if S.OnAuctionStateChanged then
            S:OnAuctionStateChanged(false)
        end
        RefreshWindow()
        return
    end

    if not S.session.active then
        return
    end

    if event == "COMMODITY_SEARCH_RESULTS_UPDATED" then
        local itemID = ...
        ApplyCommodityResults(itemID)
        return
    end

    if event == "ITEM_SEARCH_RESULTS_UPDATED" then
        local itemKey = ...
        if type(itemKey) == "table" and itemKey.itemID then
            ApplyItemResults(itemKey)
        end
        return
    end

    if event == "COMMODITY_PRICE_UPDATED" then
        local updatedUnitPrice, updatedTotalPrice = ...
        local row = S.pendingCommodityRow
        if row and row.purchaseStage == "await_price" then
            row.pendingUnitPrice = updatedUnitPrice
            row.pendingTotalPrice = updatedTotalPrice
            row.purchaseStage = "confirm"
            row.status = string.format(T("PG_SHOP_STATUS_CONFIRM_PRICE", "Confirm %s"), FormatMoneyText(updatedTotalPrice))
            RefreshWindow()
        end
        return
    end

    if event == "COMMODITY_PRICE_UNAVAILABLE" then
        local row = S.pendingCommodityRow
        if row then
            row.purchaseStage = nil
            row.pendingQuantity = nil
            row.pendingUnitPrice = nil
            row.pendingTotalPrice = nil
            row.status = T("PG_SHOP_STATUS_PRICE_UNAVAILABLE", "Price unavailable")
            S.pendingCommodityRow = nil
            RefreshWindow()
        end
        return
    end

    if event == "COMMODITY_PURCHASE_SUCCEEDED" then
        local row = S.pendingCommodityRow
        if row then
            local boughtQty = row.pendingQuantity or GetRemainingQuantity(row)

            MarkRowPurchased(
                row,
                boughtQty and boughtQty > 0
                    and string.format(T("PG_SHOP_STATUS_BOUGHT_QTY", "Bought %d"), boughtQty)
                    or T("PG_SHOP_STATUS_BOUGHT", "Bought")
            )

            S.pendingCommodityRow = nil

            if S.buyAllState.active and S.buyAllState.currentRowIndex == row.index then
                S.buyAllState.pointer = (S.buyAllState.pointer or 1) + 1
                S.buyAllState.currentRowIndex = nil
            end

            RefreshWindow()
        end
        return
    end

    if event == "COMMODITY_PURCHASED" then
        local itemID, quantity = ...
        local row = S.pendingCommodityRow
        if not row or (itemID and row.chosenItemID ~= itemID) then
            row = FindRowByItemID(itemID)
        end
        if row then
            MarkRowPurchased(
                row,
                quantity and quantity > 0
                    and string.format(T("PG_SHOP_STATUS_BOUGHT_QTY", "Bought %d"), quantity)
                    or T("PG_SHOP_STATUS_BOUGHT", "Bought")
            )

            if S.pendingCommodityRow == row then
                S.pendingCommodityRow = nil
            end

            if S.buyAllState.active and S.buyAllState.currentRowIndex == row.index then
                S.buyAllState.pointer = (S.buyAllState.pointer or 1) + 1
                S.buyAllState.currentRowIndex = nil
            end

            RefreshWindow()
        end
        return
    end

    if event == "COMMODITY_PURCHASE_FAILED" then
        local row = S.pendingCommodityRow
        if row then
            row.purchaseStage = nil
            row.pendingQuantity = nil
            row.pendingUnitPrice = nil
            row.pendingTotalPrice = nil
            row.status = T("PG_SHOP_STATUS_PURCHASE_FAILED", "Purchase failed")
            S.pendingCommodityRow = nil
            RefreshWindow()
        end
        return
    end

    if event == "AUCTION_HOUSE_PURCHASE_COMPLETED" then
        local auctionID = ...
        local row, pending = FindRowByAuctionID(auctionID)
        if row and pending then
            MarkRowPurchased(
                row,
                string.format(T("PG_SHOP_STATUS_BOUGHT_AUCTION", "Bought auction x%d"), pending.quantity or 1)
            )

            S.pendingItemPurchases[auctionID] = nil

            if S.buyAllState.active and S.buyAllState.currentRowIndex == row.index then
                S.buyAllState.pointer = (S.buyAllState.pointer or 1) + 1
                S.buyAllState.currentRowIndex = nil
            end

            RefreshWindow()
        end
        return
    end
end)
