------------------------------------------------------------
-- AuctionSlotHunter Version: 2.05
-- 在拍卖行列表中显示装备槽位与第三属性
-- Original: Hasicz (https://wago.io/V4_C0sbxw)
-- Updated for WoW 12.0.5 by 黑灬羊
--
-- v2.5 改动：
--   * 修正明细页不再用第一笔搜索结果兜底，避免同名装备串行导致等级/属性显示错误
--   * 每次刷新都会清空回收行上的 ASH 文本，避免残留旧行的装等与绿字
--
-- v2.4 改动：
--   * ASH 普通点击改为显示快速标签列，适合在装备/专业装备第一层先预选属性
--   * 进入某个物品购买明细后，按预选属性隐藏不匹配行，保留匹配行原生购买流程
--
-- v2.3 改动：
--   * 新增 ASH 标签筛选面板，可按槽位/加速/吸血/专业绿字等筛选已载入结果
--   * 筛选结果支持按价格与物品等级排序，点击结果可回到拍卖行物品搜索
--
-- v2.2 改动：
--   * 支持专业工具/配件绿字：充裕、产能、奇思、灵感、制作速度、精细、感知、灵巧
--   * 将 Enum.ItemClass.Profession 视为可显示装备
--   * 通过 C_Item.GetItemStats 兜底识别属性与宝石槽，降低对 BonusID 的依赖
--
-- v2.1 改动：
--   * Interface 兼容 12.0.7 / 12.0.5 / 12.0.1
--   * 改为 hooksecurefunc(itemList, "RefreshScrollFrame")，事件触发更准确
--   * 增加 ScrollBox.Update 兜底（适配 Blizzard 列表系统从 TableBuilder 向 ScrollView 过渡）
--   * 加固 BonusID 解析（防御 link 字段越界）
--   * 拍卖行关闭后释放 hook 标记，避免反复 reload 期间产生重复处理
------------------------------------------------------------


local ROW_HEIGHT = 22
local MAX_RESULT_ROWS = 12
-- v2.6 改动：
--   * ASH 按钮上移到拍卖行顶栏（搜索栏旁），浏览页就能直接筛选，无需先点进物品
--   * 平铺的 QuickBar 改为下拉菜单，与原版「过滤器」交互一致
--   * 多选模型：插槽 + 任一第三/绿字属性 可并列；属性之间互斥（单选）
--   * 不再覆盖物品等级文字（原生 iLvl 列已经够用）
local ActiveTags = {}     -- 当前激活的 tag 集合：tag → true
local SearchMode = "browse"
local SortMode = "priceAsc"
local ui = {}
local ProcessAllRows
-- Forward declarations：被早期函式引用，Lua 不会 hoist local。
local GetFilterLabel, FeatureMatchesTag
local IsAnyTagActive, FeatureMatchesActiveTags, RecordMatchesActiveTags
local IsDetailFilterActive, GetActiveFilterSummary
local UpdateFilterButtons, UpdateMenuItems, RefreshVisibleRows, UpdateResultRows
local HIDE_UNMATCHED_ROWS = true

-- =========================================================
-- 1. 数据表
-- =========================================================
-- 第三属性 BonusID 映射（12.0.x 现存：闪避/吸血/加速/永不磨损）
local BonusIDs = {
    ["40"] = "avoidance",      -- 闪避
    ["41"] = "leech",          -- 吸血
    ["42"] = "speed",          -- 加速
    ["43"] = "indestructible", -- 永不磨损
}

-- 通过 C_Item.GetItemStats 读取的属性 key。
-- 专业工具/配件的奇思、产能、充裕等通常不稳定依赖 BonusID，读 item stats 更稳。
local StatWatchOrder = {
    { tag = "avoidance",      key = "ITEM_MOD_CR_AVOIDANCE_SHORT",    fallback = "闪避" },
    { tag = "leech",          key = "ITEM_MOD_CR_LIFESTEAL_SHORT",    fallback = "吸血" },
    { tag = "speed",          key = "ITEM_MOD_CR_SPEED_SHORT",        fallback = "加速" },
    { tag = "indestructible",                                      fallback = "永不磨损" },
    { tag = "resourcefulness", key = "ITEM_MOD_RESOURCEFULNESS_SHORT", fallback = "充裕" },
    { tag = "multicraft",      key = "ITEM_MOD_MULTICRAFT_SHORT",      fallback = "产能" },
    { tag = "ingenuity",       key = "ITEM_MOD_INGENUITY_SHORT",       fallback = "奇思" },
    { tag = "inspiration",     key = "ITEM_MOD_INSPIRATION_SHORT",     fallback = "灵感" },
    { tag = "craftingSpeed",   key = "ITEM_MOD_CRAFTING_SPEED_SHORT",  fallback = "制作速度" },
    { tag = "finesse",         key = "ITEM_MOD_FINESSE_SHORT",         fallback = "精细" },
    { tag = "perception",      key = "ITEM_MOD_PERCEPTION_SHORT",      fallback = "感知" },
    { tag = "deftness",        key = "ITEM_MOD_DEFTNESS_SHORT",        fallback = "灵巧" },
}

local StatInfoByTag = {}
for _, statInfo in ipairs(StatWatchOrder) do
    StatInfoByTag[statInfo.tag] = statInfo
end

-- 第三属性 / 专业绿字属于「stat 类」，互斥单选。
-- socket 不在此集合，独立 toggle，可与任意 stat 并列（槽 + 闪避是常见组合）。
local STAT_TAGS_SET = {
    speed = true, leech = true, avoidance = true, indestructible = true,
    resourcefulness = true, multicraft = true, ingenuity = true, inspiration = true,
    craftingSpeed = true, finesse = true, perception = true, deftness = true,
}

local function IsStatTag(tag) return tag and STAT_TAGS_SET[tag] == true end
local function IsTagActive(tag) return tag ~= nil and ActiveTags[tag] == true end

-- 选中某 stat：先清掉其他 stat（互斥），再设当前；nil 表示清空 stat 选择。
-- socket 状态不动，保持「槽 + 任意 stat」组合可用。
local function SelectStatTag(tag)
    for t in pairs(ActiveTags) do
        if IsStatTag(t) then ActiveTags[t] = nil end
    end
    if tag then ActiveTags[tag] = true end
end

local function ToggleSocketTag()
    if ActiveTags.socket then
        ActiveTags.socket = nil
    else
        ActiveTags.socket = true
    end
end

local function ClearAllTags()
    wipe(ActiveTags)
end

-- 宝石槽 BonusID 集合
-- 注：12.0.x（Midnight）若新增宝石槽 BonusID，可向此表追加 key=true
local SocketBonusIDs = {
    ["1808"] = true, -- 棱彩宝石槽（通用）
    ["7576"] = true, -- 制造装备植入槽
    ["8809"] = true, -- 地洞钻探者槽
}

-- 颜色常量
local COLOR_BONUS  = "|cFF1EFF00"
local COLOR_ILVL   = "|cFF66CCFF"
local COLOR_END    = "|r"

-- =========================================================
-- 2. 工具函数
-- =========================================================
local function CreateInfoFontString(row)
    local fs = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    local font, _, flags = fs:GetFont()
    fs:SetFont(font, 12, flags)
    fs:Hide()
    fs:SetText("")
    return fs
end

local function IsSupportedItemClass(classID)
    if not classID then return true end
    if classID == Enum.ItemClass.Weapon or classID == Enum.ItemClass.Armor then
        return true
    end
    return Enum.ItemClass.Profession and classID == Enum.ItemClass.Profession
end

local function AddUnique(parts, seen, text)
    if not text or text == "" or seen[text] then return end
    parts[#parts + 1] = text
    seen[text] = true
end

local function GetStatLabel(tag)
    local statInfo = StatInfoByTag[tag]
    if not statInfo then return tag end
    return (statInfo.key and _G[statInfo.key]) or statInfo.fallback or tag
end

local function MergeTags(target, source)
    if not source then return end
    for tag in pairs(source) do
        target[tag] = true
    end
end

-- 解析 itemLink 中的 BonusID 列表
-- 格式: item:ID:enchant:gem1:gem2:gem3:gem4:suffix:unique:level:spec:modifiers:context:numBonus:b1:b2:...
local function ParseBonusIDs(itemLink)
    local itemString = string.match(itemLink, "item:([%d:%-]*)")
    if not itemString then return nil, nil end

    local fields = { strsplit(":", itemString) }
    local numBonus = tonumber(fields[13]) or 0
    if numBonus <= 0 then return nil, false end

    local tags, hasSocket
    -- 防御性范围检查：fields 长度可能小于 numBonus+13（损坏 link）
    local lastIdx = math.min(#fields, 13 + numBonus)
    for i = 14, lastIdx do
        local id = fields[i]
        if id and id ~= "" then
            if BonusIDs[id] then
                tags = tags or {}
                tags[BonusIDs[id]] = true
            end
            if SocketBonusIDs[id] then hasSocket = true end
        end
    end
    return tags, hasSocket
end

local function ParseItemStats(itemLink)
    if not (C_Item and C_Item.GetItemStats) then return nil, false end

    local ok, stats = pcall(C_Item.GetItemStats, itemLink)
    if not ok or type(stats) ~= "table" then return nil, false end

    local tags, hasSocket = nil, false
    for statKey in pairs(stats) do
        if type(statKey) == "string" and statKey:find("EMPTY_SOCKET", 1, true) then
            hasSocket = true
            break
        end
    end

    for _, statInfo in ipairs(StatWatchOrder) do
        if statInfo.key and stats[statInfo.key] and stats[statInfo.key] ~= 0 then
            tags = tags or {}
            tags[statInfo.tag] = true
        end
    end

    return tags, hasSocket
end

local FeatureCache = {}

local function GetItemFeatureInfo(itemLink)
    if FeatureCache[itemLink] then return FeatureCache[itemLink] end

    local tags = {}
    local bonusTags, hasSocket = ParseBonusIDs(itemLink)
    local statTags, statsHasSocket = ParseItemStats(itemLink)
    local statTexts = {}

    MergeTags(tags, bonusTags)
    MergeTags(tags, statTags)
    hasSocket = hasSocket or statsHasSocket

    for _, statInfo in ipairs(StatWatchOrder) do
        if tags[statInfo.tag] then
            statTexts[#statTexts + 1] = GetStatLabel(statInfo.tag)
        end
    end

    local featureInfo = {
        hasSocket = hasSocket,
        tags = tags,
        statTexts = statTexts,
    }
    if featureInfo.hasSocket or #featureInfo.statTexts > 0 then
        FeatureCache[itemLink] = featureInfo
    end
    return featureInfo
end

local PriceFields = {
    "minPrice",
    "price",
    "buyoutAmount",
    "unitPrice",
    "itemBuyout",
    "lowestPrice",
    "totalPrice",
}

local function ExtractPrice(data)
    if type(data) ~= "table" then return nil end

    for _, field in ipairs(PriceFields) do
        local price = tonumber(data[field])
        if price and price > 0 then return price end
    end

    if type(data.auctionInfo) == "table" then
        return ExtractPrice(data.auctionInfo)
    end
    if type(data.priceInfo) == "table" then
        return ExtractPrice(data.priceInfo)
    end

    return nil
end

local function GetItemLinkAndSearchResult(data)
    if type(data) ~= "table" then return nil, nil end

    local itemLink = data.itemLink or data.hyperlink or data.link
    if itemLink then
        return itemLink, data
    end

    local itemKey = data.itemKey
    if not (itemKey and C_AuctionHouse and C_AuctionHouse.GetItemSearchResultInfo) then
        return nil, nil
    end

    local function getSearchResult(index)
        if not index or index < 1 then return nil, nil end

        local ok, result = pcall(C_AuctionHouse.GetItemSearchResultInfo, itemKey, index)
        if ok and result then
            return result.itemLink, result
        end
        return nil, nil
    end

    local explicitIndex = tonumber(data.searchResultIndex or data.resultIndex or data.index or data.rowIndex or data.dataIndex)
    if explicitIndex then
        local link, result = getSearchResult(explicitIndex)
        if not link and explicitIndex == 0 then
            link, result = getSearchResult(1)
        end
        if link then return link, result end
    end

    local auctionID = data.auctionID or data.auctionId
    if auctionID and C_AuctionHouse.GetNumItemSearchResults then
        local ok, count = pcall(C_AuctionHouse.GetNumItemSearchResults, itemKey)
        if ok and count then
            for index = 1, count do
                local link, result = getSearchResult(index)
                if result and result.auctionID == auctionID then
                    return link, result
                end
            end
        end
    end

    return nil, nil
end

local function UnwrapAuctionData(data)
    if type(data) ~= "table" then return nil end
    return data.rowData or data.elementData or data.data or data
end

local function GetDataItemLevel(data, searchResult, itemLink)
    local level = tonumber(data and (data.itemLevel or data.level))
    if level and level > 0 then return level end

    local itemKey = data and data.itemKey or searchResult and searchResult.itemKey
    level = tonumber(itemKey and itemKey.itemLevel)
    if level and level > 0 then return level end

    if itemLink then
        local ok, detailedLevel = pcall(C_Item.GetDetailedItemLevelInfo, itemLink)
        if ok and detailedLevel and detailedLevel > 0 then return detailedLevel end
    end

    return nil
end

local function DebugDataSummary(data)
    if type(data) ~= "table" then return "nil" end

    local itemKey = data.itemKey
    local keyText = type(itemKey) == "table"
        and ("itemID=" .. tostring(itemKey.itemID)
            .. " level=" .. tostring(itemKey.itemLevel)
            .. " suffix=" .. tostring(itemKey.itemSuffix))
        or "nil"

    return "itemLink=" .. tostring(data.itemLink)
        .. " auctionID=" .. tostring(data.auctionID or data.auctionId)
        .. " index=" .. tostring(data.searchResultIndex or data.resultIndex or data.index or data.rowIndex or data.dataIndex)
        .. " level=" .. tostring(data.itemLevel or data.level)
        .. " price=" .. tostring(ExtractPrice(data))
        .. " itemKey={" .. keyText .. "}"
end

local function GetItemKeyString(itemKey)
    if type(itemKey) ~= "table" then return nil end
    return table.concat({
        tostring(itemKey.itemID or 0),
        tostring(itemKey.itemLevel or 0),
        tostring(itemKey.itemSuffix or 0),
        tostring(itemKey.battlePetSpeciesID or 0),
    }, ":")
end

local function GetLinkName(itemLink)
    if not itemLink then return nil end

    local itemName = C_Item and C_Item.GetItemInfo and C_Item.GetItemInfo(itemLink)
    if itemName then return itemName end
    return itemLink:match("%[(.-)%]") or itemLink
end

local ResultCache, ResultOrder, FilteredResults = {}, {}, {}

local function ClearResultCache()
    wipe(FeatureCache)
    wipe(ResultCache)
    wipe(ResultOrder)
    wipe(FilteredResults)
end

local function GetFeatureText(featureInfo)
    if not featureInfo then return "" end

    local parts, seen = {}, {}
    if featureInfo.hasSocket then
        AddUnique(parts, seen, "【槽】")
    end
    for _, text in ipairs(featureInfo.statTexts or {}) do
        AddUnique(parts, seen, text)
    end

    return table.concat(parts, " ")
end

local function CaptureResult(data, itemLink, ilvl, featureInfo, searchResult)
    if not itemLink or not featureInfo then return end

    local _, _, _, _, _, _, _, _, _, _, _, classID = C_Item.GetItemInfoInstant(itemLink)
    if not IsSupportedItemClass(classID) then return end

    local itemKey = data and data.itemKey or searchResult and searchResult.itemKey
    local key = itemLink or GetItemKeyString(itemKey)
    if not key then return end

    local price = ExtractPrice(data) or ExtractPrice(searchResult)
    local record = ResultCache[key]
    if not record then
        record = {}
        ResultCache[key] = record
        ResultOrder[#ResultOrder + 1] = record
    end

    record.itemLink = itemLink
    record.itemKey = itemKey
    record.name = GetLinkName(itemLink)
    if ilvl and ilvl > 0 then
        record.ilvl = ilvl
    else
        record.ilvl = record.ilvl or 0
    end
    record.price = (price and record.price) and math.min(record.price, price) or price or record.price
    record.hasSocket = featureInfo.hasSocket
    record.tags = featureInfo.tags
    record.featureText = GetFeatureText(featureInfo)
end

local function CaptureData(data)
    data = UnwrapAuctionData(data)
    if type(data) ~= "table" then return end

    local itemLink, searchResult = GetItemLinkAndSearchResult(data)
    if not itemLink then return end

    local _, _, _, _, _, _, _, _, _, _, _, classID = C_Item.GetItemInfoInstant(itemLink)
    if not IsSupportedItemClass(classID) then return end

    local ilvl = GetDataItemLevel(data, searchResult, itemLink)
    local featureInfo = GetItemFeatureInfo(itemLink)
    CaptureResult(data, itemLink, ilvl, featureInfo, searchResult)
end

local function CaptureFromDataProvider(itemList)
    if not itemList then return end

    local dataProvider
    if itemList.ScrollBox and itemList.ScrollBox.GetDataProvider then
        local ok, provider = pcall(itemList.ScrollBox.GetDataProvider, itemList.ScrollBox)
        if ok then dataProvider = provider end
    end
    dataProvider = dataProvider or itemList.dataProvider or itemList.DataProvider

    if not dataProvider then return end

    local enumerate = dataProvider.EnumerateEntireRange or dataProvider.Enumerate
    if type(enumerate) ~= "function" then return end

    local ok = pcall(function()
        local scanned = 0
        for first, second in enumerate(dataProvider) do
            local data = type(second) == "table" and second or type(first) == "table" and first
            if data then
                CaptureData(data)
                scanned = scanned + 1
                if scanned >= 500 then break end
            end
        end
    end)

    return ok
end

local function EnsureFilterMarker(row)
    if row.ashFilterMarker then return row.ashFilterMarker end

    local marker = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    marker:SetPoint("RIGHT", row, "RIGHT", -14, 0)
    marker:SetTextColor(1, 0.25, 0.2)
    marker:SetText("")
    marker:Hide()
    row.ashFilterMarker = marker
    return marker
end

local function GetRowData(row)
    if not row then return nil end
    if row.rowData then return row.rowData end
    if row.elementData then return row.elementData end
    if row.data then return row.data end
    if type(row.GetElementData) == "function" then
        local ok, data = pcall(row.GetElementData, row)
        if ok then return data end
    end
    return nil
end

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

    row:Show()
    row:SetAlpha(1)
    if row.EnableMouse then row:EnableMouse(true) end

    if row.ilvl then
        row.ilvl:SetText("")
        row.ilvl:Hide()
    end
    if row.info then
        row.info:SetText("")
        row.info:Hide()
    end
    if row.ashFilterMarker then
        row.ashFilterMarker:SetText("")
        row.ashFilterMarker:Hide()
    end
end

-- 重新锚点工具：保留所有原 anchor point，只把整体 Y 偏移到 targetY。
-- relativeTo / X 偏移不动，避免破坏 Blizzard 的横向拉伸（TOPLEFT + TOPRIGHT 配对）。
local function ShiftRowToY(row, targetY)
    local _, _, _, _, curY = row:GetPoint(1)
    curY = curY or 0
    local delta = targetY - curY
    if delta == 0 then return end

    local points = {}
    for i = 1, row:GetNumPoints() do
        points[i] = { row:GetPoint(i) }
    end
    row:ClearAllPoints()
    for _, pt in ipairs(points) do
        local p, rel, rp, x, y = pt[1], pt[2], pt[3], pt[4], pt[5]
        if rel then
            row:SetPoint(p, rel, rp, x or 0, (y or 0) + delta)
        else
            -- 没有 relativeTo（罕见）：3-arg form，避免被误用 UIParent
            row:SetPoint(p, x or 0, (y or 0) + delta)
        end
    end
end

-- 把命中过滤的行紧凑到顶部。仅在「明细页 + 有筛选」时跑，
-- 没筛选时让 Blizzard 的原生布局自己处理。hooksecurefunc 保证我们在原版 layout 之后跑，
-- 下次刷新 Blizzard 会重排，我们再 compact 一次，一来一回视觉上始终是紧凑的。
local function CompactMatchedRows(itemList)
    if not IsDetailFilterActive() then return end
    if not itemList then return end

    local rowList = {}
    if itemList.tableBuilder and itemList.tableBuilder.rows then
        for i = 1, #itemList.tableBuilder.rows do
            local r = itemList.tableBuilder.rows[i]
            if r then rowList[#rowList + 1] = r end
        end
    else
        -- ScrollBox 路径：三层守护（HasView + HasDataProvider + pcall），
        -- 防止 view 已建但 dataProvider 尚未绑定时炸到 Blizzard 内部
        local sb = itemList.ScrollBox
        if sb and type(sb.ForEachFrame) == "function"
            and (type(sb.HasView) ~= "function" or sb:HasView())
            and (type(sb.HasDataProvider) ~= "function" or sb:HasDataProvider()) then
            pcall(sb.ForEachFrame, sb, function(r)
                if r then rowList[#rowList + 1] = r end
            end)
            -- ScrollBox.ForEachFrame 不保证视觉顺序，按当前 Y 排（top → bottom）
            table.sort(rowList, function(a, b)
                local _, _, _, _, ay = a:GetPoint(1)
                local _, _, _, _, by = b:GetPoint(1)
                return (ay or 0) > (by or 0)
            end)
        end
    end

    if #rowList == 0 then return end

    local _, _, _, _, baseY = rowList[1]:GetPoint(1)
    baseY = baseY or 0

    local visibleIdx = 0
    for _, row in ipairs(rowList) do
        if row:IsShown() then
            ShiftRowToY(row, baseY - visibleIdx * ROW_HEIGHT)
            visibleIdx = visibleIdx + 1
        end
    end
end

local function ApplyNativeRowFilter(row, featureInfo)
    if not IsDetailFilterActive() then
        row:Show()
        row:SetAlpha(1)
        if row.EnableMouse then row:EnableMouse(true) end
        if row.ashFilterMarker then row.ashFilterMarker:Hide() end
        return
    end

    if FeatureMatchesActiveTags(featureInfo) then
        row:Show()
        row:SetAlpha(1)
        if row.EnableMouse then row:EnableMouse(true) end
        if row.ashFilterMarker then row.ashFilterMarker:Hide() end
    else
        if HIDE_UNMATCHED_ROWS then
            row:Hide()
            if row.EnableMouse then row:EnableMouse(false) end
            return
        end
        row:SetAlpha(0.16)
        if row.EnableMouse then row:EnableMouse(false) end
        local marker = EnsureFilterMarker(row)
        marker:SetText("非 " .. GetActiveFilterSummary())
        marker:Show()
    end
end

-- =========================================================
-- 3. 核心：刷新单行
-- =========================================================
local function RowUpdate(row)
    if not row then return end
    ResetNativeRow(row)
    if not (row.ilvl and row.info) then return end

    local data = UnwrapAuctionData(GetRowData(row))
    if not data then return end

    -- 获取 itemLink：
    -- 1. 优先 rowData.itemLink（Auctionator 等已填充）
    -- 2. 明细页仅在 rowData 有明确 index/auctionID 时才通过 API 取 link，避免错拿第一笔
    local itemLink, searchResult = GetItemLinkAndSearchResult(data)
    if not itemLink then
        if IsDetailFilterActive() then
            ApplyNativeRowFilter(row, nil)
        end
        return
    end

    -- 只对武器/防具/专业装备显示
    local _, _, _, _, _, _, _, _, _, _, _, classID = C_Item.GetItemInfoInstant(itemLink)
    if not IsSupportedItemClass(classID) then
        if IsDetailFilterActive() then
            ApplyNativeRowFilter(row, nil)
        end
        return
    end

    -- 仍读 ilvl 是为了让结果面板可以按 ilvl 排序，但不再覆盖原生 iLvl 列
    local ilvl = GetDataItemLevel(data, searchResult, itemLink)

    local featureInfo = GetItemFeatureInfo(itemLink)
    CaptureResult(data, itemLink, ilvl, featureInfo, searchResult)
    ApplyNativeRowFilter(row, featureInfo)

    -- 不再写「【槽】」文字：原生 ExtraInfo cell 自带彩色宝石格子图标，
    -- ASH 重复的黄色【槽】会盖住后面的属性字。socket 仍参与筛选（hasSocket）。
    if #featureInfo.statTexts > 0 then
        local parts, seen = {}, {}
        for _, text in ipairs(featureInfo.statTexts) do
            AddUnique(parts, seen, COLOR_BONUS .. text .. COLOR_END)
        end
        row.info:SetText(table.concat(parts, " "))
        row.info:Show()
    end
end

-- =========================================================
-- 4. 行的初始化（lazy）
-- =========================================================
local function EnsureRowFontStrings(row)
    if not row.ilvl then
        row.ilvl = CreateInfoFontString(row)
        row.ilvl:SetPoint("LEFT", row, "LEFT", 370, 0)
    end
    if not row.info then
        row.info = CreateInfoFontString(row)
        row.info:SetPoint("LEFT", row.ilvl, "RIGHT", 8, 0)
    end
end

-- =========================================================
-- 5. 筛选面板：标签 + 等级/价格排序
-- =========================================================
-- 下拉菜单的两个属性区段。装备第三属性 + 专业绿字，分别单选。
-- socket 单独由 toggle 控制，与 stat 选择正交（槽 + 闪避 是常见组合）。
local EquipmentStatTags = {
    "speed", "leech", "avoidance", "indestructible",
}
local ProfessionStatTags = {
    "resourcefulness", "multicraft", "ingenuity", "inspiration",
    "craftingSpeed", "finesse", "perception", "deftness",
}

function GetFilterLabel(tag)
    if tag == "all" then return "全部" end
    if tag == "socket" then return "插槽" end
    return GetStatLabel(tag)
end

function FeatureMatchesTag(featureInfo, tag)
    if not tag or tag == "all" then return true end
    if not featureInfo then return false end
    if tag == "socket" then return featureInfo.hasSocket end
    return featureInfo.tags and featureInfo.tags[tag]
end

function IsAnyTagActive()
    return next(ActiveTags) ~= nil
end

-- AND 逻辑：socket（如启用）+ 单选 stat 都必须命中。
function FeatureMatchesActiveTags(featureInfo)
    if not IsAnyTagActive() then return true end
    if not featureInfo then return false end
    for tag in pairs(ActiveTags) do
        if not FeatureMatchesTag(featureInfo, tag) then return false end
    end
    return true
end

function RecordMatchesActiveTags(record)
    if not IsAnyTagActive() then return true end
    if not record then return false end
    if ActiveTags.socket and not record.hasSocket then return false end
    for tag in pairs(ActiveTags) do
        if IsStatTag(tag) and not (record.tags and record.tags[tag]) then
            return false
        end
    end
    return true
end

function IsDetailFilterActive()
    return SearchMode == "item" and IsAnyTagActive()
end

function GetActiveFilterSummary()
    if not IsAnyTagActive() then return "全部" end
    local parts = {}
    if ActiveTags.socket then parts[#parts + 1] = "插槽" end
    for _, tag in ipairs(EquipmentStatTags) do
        if ActiveTags[tag] then parts[#parts + 1] = GetStatLabel(tag) end
    end
    for _, tag in ipairs(ProfessionStatTags) do
        if ActiveTags[tag] then parts[#parts + 1] = GetStatLabel(tag) end
    end
    return table.concat(parts, " + ")
end

local function RecordMatchesFilter(record)
    return RecordMatchesActiveTags(record)
end

local function SortValue(record, field)
    if field == "price" then
        return record.price
    end
    if field == "ilvl" then
        return record.ilvl or 0
    end
    return record.name or ""
end

local function CompareRecords(left, right)
    local field, descending = "price", false
    if SortMode == "priceDesc" then
        descending = true
    elseif SortMode == "ilvlAsc" then
        field = "ilvl"
    elseif SortMode == "ilvlDesc" then
        field, descending = "ilvl", true
    end

    local leftValue = SortValue(left, field)
    local rightValue = SortValue(right, field)
    if field == "price" then
        leftValue = leftValue or math.huge
        rightValue = rightValue or math.huge
    end

    if leftValue == rightValue then
        local leftPrice = left.price or math.huge
        local rightPrice = right.price or math.huge
        if leftPrice == rightPrice then
            return (left.name or "") < (right.name or "")
        end
        return leftPrice < rightPrice
    end

    if descending then
        return leftValue > rightValue
    end
    return leftValue < rightValue
end

local function RebuildFilteredResults()
    wipe(FilteredResults)
    for _, record in ipairs(ResultOrder) do
        if RecordMatchesFilter(record) then
            FilteredResults[#FilteredResults + 1] = record
        end
    end
    table.sort(FilteredResults, CompareRecords)
end

local function GetSortLabel()
    if SortMode == "priceDesc" then return "价格↓" end
    if SortMode == "ilvlAsc" then return "等级↑" end
    if SortMode == "ilvlDesc" then return "等级↓" end
    return "价格↑"
end

-- 旧的 FilterPanel 顶部 tag 按钮：根据 ActiveTags 状态点亮/灰显。
-- "all" 按钮在没有任何 tag 激活时禁用（已经是该状态）；
-- 其他按钮在 tag 已激活时禁用，避免重复点击。
local function UpdateButtonGroup(buttons)
    if not buttons then return end
    for tag, button in pairs(buttons) do
        if tag == "all" then
            button:SetEnabled(IsAnyTagActive())
        else
            button:SetEnabled(not IsTagActive(tag))
        end
    end
end

function UpdateFilterButtons()
    UpdateButtonGroup(ui.filterButtons)
    if ui.sortButton then
        ui.sortButton:SetText(GetSortLabel())
    end
    if UpdateMenuItems then UpdateMenuItems() end
    if ui.openButton then
        ui.openButton:SetText(IsAnyTagActive() and "ASH*" or "ASH")
    end
end

function RefreshVisibleRows()
    C_Timer.After(0, function()
        ProcessAllRows()
    end)
    C_Timer.After(0.05, function()
        ProcessAllRows()
    end)
end

local function ApplyTagChange()
    UpdateFilterButtons()
    if UpdateResultRows and ui.panel and ui.panel:IsShown() then
        UpdateResultRows()
    end
    RefreshVisibleRows()
end

local function FormatPrice(price)
    if not price or price <= 0 then return "-" end
    if GetMoneyString then
        return GetMoneyString(price, true)
    end
    return tostring(price)
end

local function SearchRecord(record)
    if not (record and record.itemKey and C_AuctionHouse and C_AuctionHouse.SendSearchQuery) then
        if record then
            print("|cffffcc00[ASH]|r 这条结果缺少可搜索的 itemKey，请在原拍卖行列表中点对应物品。")
        end
        return
    end

    local sorts = {}
    if Enum and Enum.AuctionHouseSortOrder and Enum.AuctionHouseSortOrder.Price then
        sorts[1] = { sortOrder = Enum.AuctionHouseSortOrder.Price, reverseSort = true }
    end
    local ok, err = pcall(C_AuctionHouse.SendSearchQuery, record.itemKey, sorts, false)
    if ok then
        print("|cff66ccff[ASH]|r 已搜索 " .. (record.itemLink or record.name or "物品"))
    else
        print("|cffff0000[ASH]|r 搜索失败: " .. tostring(err))
    end
end

function UpdateResultRows()
    if not ui.panel then return end

    RebuildFilteredResults()
    UpdateFilterButtons()

    ui.title:SetText("ASH " .. GetActiveFilterSummary() .. "：已载入 " .. #FilteredResults .. "/" .. #ResultOrder)

    local offset = 0
    if FauxScrollFrame_Update and FauxScrollFrame_GetOffset then
        FauxScrollFrame_Update(ui.scrollFrame, #FilteredResults, MAX_RESULT_ROWS, ROW_HEIGHT)
        offset = FauxScrollFrame_GetOffset(ui.scrollFrame) or 0
    end

    for i, row in ipairs(ui.rows) do
        local record = FilteredResults[offset + i]
        row.record = record
        if record then
            row.name:SetText(record.itemLink or record.name or "")
            row.ilvl:SetText(record.ilvl and record.ilvl > 0 and tostring(record.ilvl) or "-")
            row.price:SetText(FormatPrice(record.price))
            row.stats:SetText(record.featureText or "")
            row:Show()
        else
            row:Hide()
        end
    end
end

local function ShowFilterPanel()
    if ui.panel then
        ui.panel:Show()
        UpdateResultRows()
    end
    RefreshVisibleRows()
end

local function CreateSmallButton(parent, text, width, onClick)
    local button = CreateFrame("Button", nil, parent, "UIPanelButtonTemplate")
    button:SetSize(width or 54, 20)
    button:SetText(text)
    button:SetScript("OnClick", onClick)
    return button
end

local function CreateResultRow(parent, index)
    local row = CreateFrame("Button", nil, parent)
    row:SetSize(610, ROW_HEIGHT)
    if index == 1 then
        row:SetPoint("TOPLEFT", parent, "TOPLEFT", 8, -118)
    else
        row:SetPoint("TOPLEFT", parent.rows[index - 1], "BOTTOMLEFT", 0, 0)
    end

    row.bg = row:CreateTexture(nil, "BACKGROUND")
    row.bg:SetAllPoints()
    row.bg:SetColorTexture(0.08, 0.08, 0.08, index % 2 == 0 and 0.45 or 0.25)

    row.name = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    row.name:SetPoint("LEFT", row, "LEFT", 4, 0)
    row.name:SetSize(270, ROW_HEIGHT)
    row.name:SetJustifyH("LEFT")
    row.name:SetWordWrap(false)

    row.ilvl = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    row.ilvl:SetPoint("LEFT", row.name, "RIGHT", 8, 0)
    row.ilvl:SetSize(46, ROW_HEIGHT)
    row.ilvl:SetJustifyH("RIGHT")
    row.ilvl:SetTextColor(0.4, 0.8, 1)

    row.price = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    row.price:SetPoint("LEFT", row.ilvl, "RIGHT", 12, 0)
    row.price:SetSize(128, ROW_HEIGHT)
    row.price:SetJustifyH("RIGHT")

    row.stats = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    row.stats:SetPoint("LEFT", row.price, "RIGHT", 14, 0)
    row.stats:SetSize(132, ROW_HEIGHT)
    row.stats:SetJustifyH("LEFT")
    row.stats:SetTextColor(0.12, 1, 0)
    row.stats:SetWordWrap(false)

    row:SetScript("OnEnter", function(self)
        if not (self.record and self.record.itemLink) then return end
        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        GameTooltip:SetHyperlink(self.record.itemLink)
        GameTooltip:AddLine(" ")
        GameTooltip:AddLine("左键：在拍卖行搜索该物品", 0.4, 0.8, 1)
        GameTooltip:Show()
    end)
    row:SetScript("OnLeave", function()
        GameTooltip:Hide()
    end)
    row:SetScript("OnClick", function(self)
        if IsModifiedClick("CHATLINK") and self.record and self.record.itemLink and ChatEdit_InsertLink(self.record.itemLink) then
            return
        end
        SearchRecord(self.record)
    end)

    return row
end

-- =========================================================
-- 5b. 下拉菜单：单 row（hover 高亮 + ●/○ 表示状态）
-- =========================================================
local function CreateMenuRow(parent, label, isActive, onClick, indent)
    local row = CreateFrame("Button", nil, parent)
    row:SetSize(184, 20)
    row.hover = row:CreateTexture(nil, "BACKGROUND")
    row.hover:SetAllPoints()
    row.hover:SetColorTexture(0.4, 0.6, 1, 0)
    row.text = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    row.text:SetPoint("LEFT", row, "LEFT", indent or 8, 0)
    row.text:SetJustifyH("LEFT")
    row.refresh = function()
        if isActive() then
            row.text:SetTextColor(1, 0.82, 0)
            row.text:SetText("● " .. label)
        else
            row.text:SetTextColor(1, 1, 1)
            row.text:SetText("○ " .. label)
        end
    end
    row:SetScript("OnEnter", function() row.hover:SetColorTexture(0.4, 0.6, 1, 0.18) end)
    row:SetScript("OnLeave", function() row.hover:SetColorTexture(0, 0, 0, 0) end)
    row:SetScript("OnClick", function() onClick() end)
    row.refresh()
    return row
end

local function CreateMenuLabel(parent, text)
    local fs = parent:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall")
    fs:SetSize(180, 14)
    fs:SetJustifyH("LEFT")
    fs:SetText(text)
    return fs
end

-- 用于 stat 行：再点一次自身 = 取消选择；点别的 stat = 切换
local function ToggleStatTag(tag)
    if IsTagActive(tag) then
        SelectStatTag(nil)
    else
        SelectStatTag(tag)
    end
end

local function EnsureFilterUI()
    if ui.panel then return end

    local frame = AuctionHouseFrame
    if not frame then return end

    -- 浏览页就要能看到 ASH 按钮 → 钉到 AH 顶栏（搜索栏 / 过滤器旁）
    local searchBar = frame.SearchBar
    local parent = searchBar or frame

    ui.openButton = CreateSmallButton(parent, "ASH", 46, function()
        if IsShiftKeyDown() then
            if ui.panel:IsShown() then
                ui.panel:Hide()
            else
                ShowFilterPanel()
            end
            return
        end
        if ui.menu:IsShown() then
            ui.menu:Hide()
        else
            ui.menu:Show()
            UpdateMenuItems()
        end
    end)
    if searchBar and searchBar.FilterButton then
        ui.openButton:SetPoint("RIGHT", searchBar.FilterButton, "LEFT", -4, 0)
    elseif searchBar then
        ui.openButton:SetPoint("RIGHT", searchBar, "RIGHT", -8, 0)
    else
        -- 兜底：贴在拍卖行右上角，找不到 SearchBar 也至少能用
        ui.openButton:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -90, -42)
    end

    -- ========= 下拉菜单 =========
    ui.menu = CreateFrame("Frame", "AuctionSlotHunterDropdownMenu", parent, "BackdropTemplate")
    ui.menu:SetFrameStrata("DIALOG")
    ui.menu:SetBackdrop({
        bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
        edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
        tile = true,
        tileSize = 32,
        edgeSize = 16,
        insets = { left = 4, right = 4, top = 4, bottom = 4 },
    })
    ui.menu:SetPoint("TOPRIGHT", ui.openButton, "BOTTOMRIGHT", 0, -2)
    ui.menu:SetSize(200, 420)
    ui.menu:Hide()
    ui.menu:EnableMouse(true)

    local refreshFns = {}
    local function addRow(y, label, isActive, onClick, indent)
        local row = CreateMenuRow(ui.menu, label, isActive, function()
            onClick()
            ApplyTagChange()
        end, indent)
        row:SetPoint("TOPLEFT", ui.menu, "TOPLEFT", 6, y)
        refreshFns[#refreshFns + 1] = row.refresh
        return row
    end
    local function addLabel(y, text)
        local fs = CreateMenuLabel(ui.menu, text)
        fs:SetPoint("TOPLEFT", ui.menu, "TOPLEFT", 10, y)
    end

    local y = -10
    addRow(y, "全部（清空筛选）",
        function() return not IsAnyTagActive() end,
        function() ClearAllTags() end)
    y = y - 22

    addRow(y, "插槽（可与下方任一属性并选）",
        function() return IsTagActive("socket") end,
        function() ToggleSocketTag() end)
    y = y - 26

    addLabel(y, "装备第三属性（单选）")
    y = y - 16
    for _, tag in ipairs(EquipmentStatTags) do
        addRow(y, GetStatLabel(tag),
            function() return IsTagActive(tag) end,
            function() ToggleStatTag(tag) end, 18)
        y = y - 20
    end
    y = y - 6

    addLabel(y, "专业绿字（单选）")
    y = y - 16
    for _, tag in ipairs(ProfessionStatTags) do
        addRow(y, GetStatLabel(tag),
            function() return IsTagActive(tag) end,
            function() ToggleStatTag(tag) end, 18)
        y = y - 20
    end
    y = y - 8

    local hint = ui.menu:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall")
    hint:SetPoint("TOPLEFT", ui.menu, "TOPLEFT", 8, y)
    hint:SetSize(184, 14)
    hint:SetJustifyH("LEFT")
    hint:SetText("Shift + 点 ASH：打开结果面板")
    y = y - 18

    ui.menu:SetHeight(math.abs(y) + 6)

    UpdateMenuItems = function()
        for _, fn in ipairs(refreshFns) do fn() end
    end

    -- ========= FilterPanel（Shift+ASH 打开的大型结果面板） =========
    local panel = CreateFrame("Frame", "AuctionSlotHunterFilterPanel", parent, "BackdropTemplate")
    panel:SetSize(640, 396)
    panel:SetFrameStrata("HIGH")
    panel:SetBackdrop({
        bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
        edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
        tile = true,
        tileSize = 32,
        edgeSize = 18,
        insets = { left = 5, right = 5, top = 5, bottom = 5 },
    })
    panel:SetPoint("TOP", frame, "TOP", 0, -80)
    panel:Hide()
    ui.panel = panel

    ui.title = panel:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    ui.title:SetPoint("TOPLEFT", panel, "TOPLEFT", 12, -12)
    ui.title:SetSize(360, 18)
    ui.title:SetJustifyH("LEFT")

    ui.closeButton = CreateSmallButton(panel, "关闭", 46, function()
        panel:Hide()
    end)
    ui.closeButton:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -12, -10)

    ui.clearButton = CreateSmallButton(panel, "清空", 46, function()
        ClearResultCache()
        UpdateResultRows()
    end)
    ui.clearButton:SetPoint("RIGHT", ui.closeButton, "LEFT", -4, 0)

    ui.sortButton = CreateSmallButton(panel, GetSortLabel(), 58, function()
        if SortMode == "priceAsc" then
            SortMode = "priceDesc"
        elseif SortMode == "priceDesc" then
            SortMode = "ilvlDesc"
        elseif SortMode == "ilvlDesc" then
            SortMode = "ilvlAsc"
        else
            SortMode = "priceAsc"
        end
        UpdateResultRows()
    end)
    ui.sortButton:SetPoint("RIGHT", ui.clearButton, "LEFT", -4, 0)

    -- 面板顶部的 tag 按钮：复用同样的 ActiveTags 状态
    local PanelFilterDefs = {
        { tag = "all", label = "全部", width = 44 },
        { tag = "socket", label = "插槽", width = 44 },
        { tag = "speed", width = 48 },
        { tag = "leech", width = 48 },
        { tag = "avoidance", width = 48 },
        { tag = "indestructible", width = 68 },
        { tag = "resourcefulness", width = 48 },
        { tag = "multicraft", width = 48 },
        { tag = "ingenuity", width = 48 },
        { tag = "inspiration", width = 48 },
        { tag = "craftingSpeed", width = 68 },
        { tag = "finesse", width = 48 },
        { tag = "perception", width = 48 },
        { tag = "deftness", width = 48 },
    }

    ui.filterButtons = {}
    local x, py = 12, -38
    for _, def in ipairs(PanelFilterDefs) do
        local tag = def.tag
        local label = def.label or GetFilterLabel(tag)
        local button = CreateSmallButton(panel, label, def.width, function()
            if tag == "all" then
                ClearAllTags()
            elseif tag == "socket" then
                ToggleSocketTag()
            else
                ToggleStatTag(tag)
            end
            ApplyTagChange()
        end)
        if x + def.width > 612 then
            x = 12
            py = py - 24
        end
        button:SetPoint("TOPLEFT", panel, "TOPLEFT", x, py)
        x = x + def.width + 4
        ui.filterButtons[tag] = button
    end

    local header = panel:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall")
    header:SetPoint("TOPLEFT", panel, "TOPLEFT", 12, -96)
    header:SetSize(600, 16)
    header:SetJustifyH("LEFT")
    header:SetText("物品                                                    等级          价格                         属性")

    panel.rows = {}
    ui.rows = panel.rows
    for i = 1, MAX_RESULT_ROWS do
        panel.rows[i] = CreateResultRow(panel, i)
    end

    ui.scrollFrame = CreateFrame("ScrollFrame", "AuctionSlotHunterResultScrollFrame", panel, "FauxScrollFrameTemplate")
    ui.scrollFrame:SetPoint("TOPLEFT", panel.rows[1], "TOPRIGHT", 2, 0)
    ui.scrollFrame:SetSize(24, ROW_HEIGHT * MAX_RESULT_ROWS)
    ui.scrollFrame:SetScript("OnVerticalScroll", function(self, offset)
        if FauxScrollFrame_OnVerticalScroll then
            FauxScrollFrame_OnVerticalScroll(self, offset, ROW_HEIGHT, UpdateResultRows)
        end
    end)

    UpdateFilterButtons()
end

-- =========================================================
-- 6. 列表刷新触发：tableBuilder.rows + ScrollBox 双管齐下
-- =========================================================
function ProcessAllRows()
    local frame = AuctionHouseFrame
    if not frame then return end
    local itemList = frame.ItemBuyFrame and frame.ItemBuyFrame.ItemList
    if not itemList then return end

    EnsureFilterUI()
    CaptureFromDataProvider(itemList)

    -- 路径 A：传统 TableBuilder（12.0.x 仍可用）
    if itemList.tableBuilder and itemList.tableBuilder.rows then
        for _, row in pairs(itemList.tableBuilder.rows) do
            EnsureRowFontStrings(row)
            RowUpdate(row)
        end
        CompactMatchedRows(itemList)
        if ui.panel and ui.panel:IsShown() then UpdateResultRows() end
        return
    end

    -- 路径 B：现代 ScrollBox（向后兼容）
    -- 注意：HasView() 只保证 view 对象存在，不保证已经绑定 dataProvider；
    -- 在 AH 刚打开、尚未搜索时直接调 ForEachFrame 会触发
    -- Blizzard_SharedXML/Scroll/ScrollBox.lua:591 attempt to index a nil value
    local sb = itemList.ScrollBox
    if sb and type(sb.ForEachFrame) == "function"
        and (type(sb.HasView) ~= "function" or sb:HasView())
        and (type(sb.HasDataProvider) ~= "function" or sb:HasDataProvider()) then
        pcall(sb.ForEachFrame, sb, function(row)
            if row then
                EnsureRowFontStrings(row)
                RowUpdate(row)
            end
        end)
    end
    CompactMatchedRows(itemList)
    if ui.panel and ui.panel:IsShown() then UpdateResultRows() end
end

-- =========================================================
-- 7. Hook 拍卖行刷新（AH 打开时安装一次）
-- =========================================================
local hookInstalled = false
local queryHooksInstalled = false
local function InstallAuctionHooks()
    if hookInstalled then return end
    local frame = AuctionHouseFrame
    if not frame then return end
    local itemList = frame.ItemBuyFrame and frame.ItemBuyFrame.ItemList
    if not itemList then return end

    -- ItemList:RefreshScrollFrame 是列表内容真正刷新的核心入口
    if type(itemList.RefreshScrollFrame) == "function" then
        hooksecurefunc(itemList, "RefreshScrollFrame", ProcessAllRows)
    end
    -- 兜底：ScrollBox 滚动重排时也触发（向后兼容 ScrollView 过渡）
    if itemList.ScrollBox and type(itemList.ScrollBox.Update) == "function" then
        hooksecurefunc(itemList.ScrollBox, "Update", ProcessAllRows)
    end

    if C_AuctionHouse and not queryHooksInstalled then
        local function ClearForNewQuery(mode)
            SearchMode = mode or "browse"
            ClearResultCache()
            UpdateFilterButtons()
            if ui.panel and ui.panel:IsShown() then UpdateResultRows() end
        end
        if type(C_AuctionHouse.SendSearchQuery) == "function" then
            pcall(hooksecurefunc, C_AuctionHouse, "SendSearchQuery", function()
                ClearForNewQuery("item")
            end)
        end
        for _, functionName in ipairs({ "SendBrowseQuery", "SearchForItemKeys" }) do
            if type(C_AuctionHouse[functionName]) == "function" then
                pcall(hooksecurefunc, C_AuctionHouse, functionName, function()
                    ClearForNewQuery("browse")
                end)
            end
        end
        queryHooksInstalled = true
    end

    hookInstalled = true
end

-- =========================================================
-- 8. 事件管线
-- =========================================================
local frame = CreateFrame("Frame")
frame:RegisterEvent("AUCTION_HOUSE_SHOW")
frame:RegisterEvent("AUCTION_HOUSE_CLOSED")
frame:RegisterEvent("ITEM_SEARCH_RESULTS_UPDATED")
frame:RegisterEvent("AUCTION_HOUSE_BROWSE_RESULTS_ADDED")
frame:RegisterEvent("AUCTION_HOUSE_BROWSE_RESULTS_UPDATED")

frame:SetScript("OnEvent", function(self, event)
    if event == "AUCTION_HOUSE_SHOW" then
        -- AH 框架在 SHOW 时才完成 ItemBuyFrame 创建，此时安装 hook 最稳
        SearchMode = "browse"
        InstallAuctionHooks()
        EnsureFilterUI()
        ProcessAllRows()
    elseif event == "AUCTION_HOUSE_CLOSED" then
        SearchMode = "browse"
        ClearResultCache()
        if ui.panel then ui.panel:Hide() end
        if ui.menu then ui.menu:Hide() end
    elseif event == "ITEM_SEARCH_RESULTS_UPDATED" then
        SearchMode = "item"
        UpdateFilterButtons()
        C_Timer.After(0, ProcessAllRows)
        C_Timer.After(0.05, ProcessAllRows)
    elseif event == "AUCTION_HOUSE_BROWSE_RESULTS_ADDED"
        or event == "AUCTION_HOUSE_BROWSE_RESULTS_UPDATED" then
        SearchMode = "browse"
        UpdateFilterButtons()
        -- API 拉到新结果后给一帧延迟让 ScrollBox 重排，再处理
        C_Timer.After(0, ProcessAllRows)
        C_Timer.After(0.05, ProcessAllRows)
    end
end)

-- =========================================================
-- 9. 调试命令：/ash debug
-- =========================================================
SLASH_ASLOTHUNTER1 = "/ash"
SlashCmdList["ASLOTHUNTER"] = function(msg)
    if msg ~= "debug" then
        print("|cff66ccff[ASH]|r 用法：/ash debug（在拍卖行搜索结果页执行）")
        return
    end
    local itemList = AuctionHouseFrame
        and AuctionHouseFrame.ItemBuyFrame
        and AuctionHouseFrame.ItemBuyFrame.ItemList
    if not itemList then
        print("|cffff0000[ASH]|r 拍卖行未打开或列表为空")
        return
    end

    local rows
    if itemList.tableBuilder and itemList.tableBuilder.rows then
        rows = itemList.tableBuilder.rows
        print("|cff66ccff[ASH]|r 路径：tableBuilder.rows")
    elseif itemList.ScrollBox then
        print("|cff66ccff[ASH]|r 路径：ScrollBox（无 tableBuilder）")
        local count = 0
        rows = {}
        local sb = itemList.ScrollBox
        local hasView = (type(sb.HasView) ~= "function") or sb:HasView()
        local hasDP   = (type(sb.HasDataProvider) ~= "function") or sb:HasDataProvider()
        if hasView and hasDP then
            pcall(sb.ForEachFrame, sb, function(row)
                count = count + 1
                if #rows < 4 then
                    rows[#rows + 1] = row
                end
            end)
        else
            print(("[ASH] ScrollBox 尚未就绪：HasView=%s HasDataProvider=%s"):format(tostring(hasView), tostring(hasDP)))
        end
        print("[ASH] 可见行数: " .. count)
    end

    if not rows then return end

    local count = 0
    for k, row in pairs(rows) do
        count = count + 1
        if count <= 2 then
            local d = UnwrapAuctionData(GetRowData(row))
            if not d then
                print("[ASH] row[" .. k .. "] rowData=nil")
            else
                local link = GetItemLinkAndSearchResult(d)
                print("[ASH] row[" .. k .. "] link=" .. tostring(link))
                print("  data=" .. DebugDataSummary(d))

                if link then
                    local _, result = GetItemLinkAndSearchResult(d)
                    local _, _, _, _, _, _, _, _, _, _, _, classID = C_Item.GetItemInfoInstant(link)
                    print("  classID=" .. tostring(classID)
                        .. " Weapon=" .. tostring(Enum.ItemClass.Weapon)
                        .. " Armor=" .. tostring(Enum.ItemClass.Armor)
                        .. " Profession=" .. tostring(Enum.ItemClass.Profession))
                    print("  displayIlvl=" .. tostring(GetDataItemLevel(d, result, link)))

                    local featureInfo = GetItemFeatureInfo(link)
                    print("  hasSocket=" .. tostring(featureInfo.hasSocket))
                    print("  statTexts=" .. tostring(featureInfo.statTexts and table.concat(featureInfo.statTexts, ", ") or nil))
                end
                print("  row.ilvl=" .. tostring(row.ilvl) .. " row.info=" .. tostring(row.info))
            end
        end
    end
    print("[ASH] 共 " .. count .. " 行")
end
