-- ==========================================
-- QuickAuctionBuyer - 拍卖行购物助手
--
-- 衷心感谢 NGA 论坛 冰摇柠檬、雪白的黑牛 两位基于 WeakAuras 制作的版本。
-- 本插件改用魔兽世界原生 UI 模板（ButtonFrameTemplate / PanelTabButtonTemplate
-- / UIPanelScrollFrameTemplate / 原生纹理）重制，零外部美术。
-- ==========================================

-- ==========================================
-- 布局常量
-- ==========================================
-- 需容纳单行最多 9 个图标：宽 ≈ 9*ICON_SIZE + 8*ICON_HGAP，随 ICON_HGAP 调整 WINDOW_WIDTH
local WINDOW_WIDTH       = 520
local WINDOW_HEIGHT      = 540

local ICON_SIZE          = 37            -- ItemButtonTemplate 默认尺寸
local ICON_HGAP          = 14            -- 图标列之间水平间距（略宽，不挤）
local ICON_VGAP          = 26            -- 图标行与行之间的纵向间距（含图标下方文字标签）
local LABEL_GAP          = 4             -- 图标到下方文字的间距
local SECTION_HEADER_H   = 22            -- 子类别标题栏高度

-- 垂直节奏：「标题 ↔ 图标行 ↔ 下一标题」使用同一间距（像素）
local SECTION_V_SPACING    = 16
-- 底部 PanelTab 相对主窗 BOTTOMLEFT 的水平偏移（增大则整体右移，便于对齐官方拍卖行底栏）
local TAB_BAR_LEFT_OFFSET  = 28

-- ------------------------------------------------------------------
-- 留白：
--   FRAME_INSET_MARGIN — 深黑 Inset 与外层金属框
--   CONTENT_MARGIN     — 滚动内容区左/上/底与 Inset 内缘；右侧由滚动条贴齐，不再额外留缝
-- ------------------------------------------------------------------
local FRAME_INSET_MARGIN = 10
local CONTENT_MARGIN     = 22
local SCROLLBAR_WIDTH    = 28            -- 原生滚动条占位宽度

-- 滚动子内容宽度：左留白 + 可视区 + 滚动条贴右
local CONTENT_WIDTH = WINDOW_WIDTH - 2 * FRAME_INSET_MARGIN - CONTENT_MARGIN - SCROLLBAR_WIDTH

-- ==========================================
-- 颜色常量（沿用 WoW 标准色）
-- ==========================================
local COLOR_TEXT_NORMAL    = {1.0, 0.82, 0.0}       -- 金色（标题/选中）
local COLOR_TEXT_LIGHT     = {1.0, 1.0, 1.0}        -- 白色（正常文字）
local COLOR_TEXT_DIM       = {0.5, 0.5, 0.5}        -- 灰色（背包中没有）
local COLOR_COUNT          = {0.1, 1.0, 0.1}        -- 绿色（数量提示）
-- 追踪配方「专业」副标题（与说明图 #9d9d9d 接近的灰字）
local COLOR_TRACKED_PROF_SUBTITLE = { 157 / 255, 157 / 255, 157 / 255 }

-- 统一比当前模板字号小 1px（最小 8）
local function FontDownOne(fs)
    if not fs or not fs.GetFont then return end
    local fontFile, size, flags = fs:GetFont()
    if fontFile and size then
        fs:SetFont(fontFile, math.max(8, math.floor(size + 0.5) - 1), flags or "")
    end
end

-- ==========================================
-- 数据声明
-- ==========================================
local TAB_DEFS = {
    {key = "home",       name = "首页"},
    {key = "consumable", name = "消耗品"},
    {key = "enchant",    name = "装备附魔"},
    {key = "gem",        name = "公函与宝石"},
    {key = "about",      name = "关于"},
}

local categoryData = {
    consumable = {},
    enchant    = {},
    gem        = {},
}

local function AddCategory(tabKey, title, buttons)
    table.insert(categoryData[tabKey], {title = title, buttons = buttons})
end

-- ==========================================
-- 数据填充：消耗品
-- ==========================================
AddCategory("consumable", "药水", {
    {label = "红药",   search = "银月城生命药水",  id = {241304, 241305}},
    {label = "蓝药",   search = "光注法力药水",    id = {241300, 241301}},
    {label = "双回",   search = "复苏血清",        id = {241306, 241307}},
    {label = "护盾",   search = "圣光之护",        id = {241286, 241287}},
    {label = "隐身",   search = "虚空遮蔽酊剂",    id = {241302, 241303}},
    {label = "主属性", search = "圣光潜力",        id = {241308, 241309}},
    {label = "主狂",   search = "狂放恣意饮剂",    id = {241292, 241293}},
    {label = "绿字",   search = "鲁莽药水",        id = {241288, 241289}},
    {label = "单体",   search = "狂热药水",        id = {241296, 241297}},
})

AddCategory("consumable", "合剂/武器加强", {
    {label = "暴击", search = "破碎残阳合剂",   id = {241326, 241327}},
    {label = "急速", search = "血骑士合剂",     id = {241324, 241325}},
    {label = "全能", search = "萨拉斯抗性合剂", id = {241320, 241321}},
    {label = "精通", search = "魔导师合剂",     id = {241322, 241323}},
    {label = "平衡石", search = "辉耀平衡石",         id = {237367, 237369}},
    {label = "磨刀石", search = "辉耀磨刀石",         id = {237370, 237371}},
    {label = "绿字",   search = "萨拉斯凤凰之油",     id = {243734, 243733}},
    {label = "治疗",   search = "黎明之油",           id = {243735, 243736}},
    {label = "奥术",   search = "私运者的附魔之锋",   id = {243737, 243738}},
})

AddCategory("consumable", "食物/符文", {
    {label = "主属性", search = "异乎寻常的皇家烤肉", id = 255847},
    {label = "副属性", search = "勇士便当",           id = 242274},
    {label = "符文",   search = "虚触强化符文",       id = 259085},
    {label = "主大餐", search = "银月城浮华大餐",     id = 255845},
    {label = "副大餐", search = "盛放筵席",           id = 242273},
})

AddCategory("consumable", "其他", {
    {label = "战复",   search = "应急灵魂链接",         id = {248486, 269586}},
    {label = "修理",   search = "自动铁锤",             id = 132514},
    {label = "开锁",   search = "萨拉斯万能钥匙",       id = 260232},
    {label = "银月",   search = "合约：银月宫廷",       id = {245799, 245800}},
    {label = "奇点",   search = "合约：奇点特勤",       id = {245793, 245794}},
    {label = "哈籁提", search = "合约：哈籁提",         id = {245795, 245796}},
    {label = "阿曼尼", search = "合约：阿曼尼部族",     id = {245797, 245798}},
})

-- ==========================================
-- 数据填充：装备附魔
-- ==========================================
AddCategory("enchant", "附魔:戒指", {
    {label = "暴击", search = "附魔戒指 - 自然之怒",     id = {243986, 243987}},
    {label = "急速", search = "附魔戒指 - 银月城之捷",   id = {244014, 244015}},
    {label = "精通", search = "附魔戒指 - 祖尔金的精通", id = {243958, 243959}},
    {label = "全能", search = "附魔戒指 - 银月城之韧",   id = {244016, 244017}},
    {label = "暴伤", search = "附魔戒指 - 鹰眼神视",     id = {243956, 243957}},
})

AddCategory("enchant", "附魔:武器", {
    {label = "主属性", search = "附魔武器 - 朗多雷之锐",       id = {244028, 244029}},
    {label = "暴击",   search = "附魔武器 - 加亚莱的精准",     id = {243970, 243971}},
    {label = "急速",   search = "附魔武器 - 狂战士之怒",       id = {243972, 243973}},
    {label = "精通",   search = "附魔武器 - 奥术精通",         id = {244030, 244031}},
    {label = "全能",   search = "附魔武器 - 世界之魂的坚韧",   id = {244000, 244001}},
    {label = "护盾",   search = "附魔武器 - 世界之魂的庇护",   id = {243998, 243999}},
    {label = "治疗",   search = "附魔武器 - 世界之魂的摇篮",   id = {243996, 243997}},
    {label = "流血",   search = "附魔武器 - 哈尔拉兹之力",     id = {243968, 243969}},
    {label = "AOE",    search = "附魔武器 - 辛多雷之焰",       id = {244026, 244027}},
})

AddCategory("enchant", "附魔:头盔", {
    {label = "吸血", search = "附魔头盔 - 强化吸血妖术", id = {243950, 243951}},
    {label = "闪避", search = "附魔头盔 - 强化闪避符文", id = {244006, 244007}},
    {label = "加速", search = "附魔头盔 - 强化加速祝福", id = {243980, 243981}},
})

AddCategory("enchant", "附魔:胸甲", {
    {label = "主属性", search = "附魔胸甲 - 世界之魂印记", id = {243976, 243977}},
    {label = "敏捷",   search = "附魔胸甲 - 护根者印记",   id = {243974, 243975}},
    {label = "力耐",   search = "附魔胸甲 - 纳洛拉克印记", id = {243946, 243947}},
    {label = "智法",   search = "附魔胸甲 - 魔导师印记",   id = {244002, 244003}},
})

AddCategory("enchant", "附魔:护肩", {
    {label = "吸血", search = "附魔护肩 - 银月城治愈",     id = {244020, 244021}},
    {label = "闪避", search = "附魔护肩 - 阿梅达希尔之赐", id = {243990, 243991}},
    {label = "加速", search = "附魔护肩 - 埃基尔松的迅捷", id = {243962, 243963}},
})

AddCategory("enchant", "附魔:腿部", {
    {label = "力敏耐", search = "森林猎手的护甲片", id = {244640, 244641}},
    {label = "力敏甲", search = "血骑士的护甲片",   id = {244642, 244643}},
    {label = "智法",   search = "奥纹魔线",         id = {240154, 240155}},
})

-- ==========================================
-- 数据填充：公函与宝石
-- ==========================================
AddCategory("gem", "萨拉斯公函", {
    {label = "全 急", search = "曙光之萨拉斯公函", id = {245781, 245782}},
    {label = "急 精", search = "灼光之萨拉斯公函", id = {245783, 245784}},
    {label = "急 暴", search = "燎火之萨拉斯公函", id = {245785, 245786}},
    {label = "暴 精", search = "无双之萨拉斯公函", id = {245789, 245790}},
    {label = "暴 全", search = "快刀之萨拉斯公函", id = {245791, 245792}},
    {label = "全 精", search = "谐律之萨拉斯公函", id = {245787, 245788}},
})

AddCategory("gem", "美化材料", {
    {label = "穿山甲", search = "圣佑穿山甲护符",   id = {244603, 244604}},
    {label = "孢子",   search = "原始孢子缚带",     id = {244607, 244608}},
    {label = "吞噬",   search = "吞噬绑带",         id = {244674, 244675}},
    {label = "鲜血",   search = "暗月徽记：鲜血",   id = {245871, 245872}},
    {label = "狩猎",   search = "暗月徽记：狩猎",   id = {245875, 245876}},
    {label = "腐烂",   search = "暗月徽记：腐烂",   id = {245877, 245878}},
    {label = "虚空",   search = "暗月徽记：虚空",   id = {245873, 245874}},
    {label = "幸运",   search = "幸运钥匙串",       id = 248130},
})

AddCategory("gem", "永歌钻石", {
    {label = "主属性", search = "费解之永歌钻石", id = {240982, 240983}},
    {label = "暴伤",   search = "强能之永歌钻石", id = {240966, 240967}},
    {label = "法力",   search = "御土之永歌钻石", id = {240968, 240969}},
    {label = "护甲",   search = "坚韧之永歌钻石", id = {240970, 240971}},
})

AddCategory("gem", "急速/全能宝石", {
    {label = "全能", search = "无瑕万能榄石",   id = {240893, 240894}},
    {label = "精通", search = "无瑕精湛榄石",   id = {240891, 240892}},
    {label = "暴击", search = "无瑕致命榄石",   id = {240889, 240890}},
    {label = "急速", search = "无瑕迅捷青金石", id = {240915, 240916}},
    {label = "暴击", search = "无瑕致命青金石", id = {240913, 240914}},
    {label = "精通", search = "无瑕精湛青金石", id = {240917, 240918}},
})

AddCategory("gem", "暴击/精通宝石", {
    {label = "急速", search = "无瑕迅捷榴石", id = {240905, 240906}},
    {label = "全能", search = "无瑕万能榴石", id = {240909, 240910}},
    {label = "精通", search = "无瑕精湛榴石", id = {240907, 240908}},
    {label = "急速", search = "无瑕迅捷紫晶", id = {240899, 240900}},
    {label = "暴击", search = "无瑕致命紫晶", id = {240897, 240898}},
    {label = "全能", search = "无瑕万能紫晶", id = {240901, 240902}},
})

AddCategory("gem", "工程齿轮", {
    {label = "暴击", search = "通量齿轮", id = {244697, 244698}},
    {label = "急速", search = "滑油齿轮", id = {244699, 244700}},
    {label = "全能", search = "吻合齿轮", id = {244703, 244704}},
    {label = "精通", search = "完美齿轮", id = {244701, 244702}},
})

-- ==========================================
-- 工具函数
-- ==========================================
local function GetItemCountSafe(itemID)
    if C_Item and C_Item.GetItemCount then
        return C_Item.GetItemCount(itemID, false, false, false) or 0
    elseif GetItemCount then
        return GetItemCount(itemID) or 0
    end
    return 0
end

-- 物品名在客户端未缓存时 GetItemInfo 第一返回值会为 nil；先显示占位并 RequestLoadItemDataByID，见 frame 创建后的实现。
local qabPendingTrackItemIDs = {}
local qabProfItemRefreshTimer
local GetItemNameSafe
-- 供 RefreshHomeProfessionsSection 在 CreateIconButton 定义之前引用
local RefreshHomeProfessionsSection
local CreateIconButton

local function GetItemIconSafe(itemID)
    if not itemID then return nil end
    if C_Item and C_Item.GetItemIconByID then
        return C_Item.GetItemIconByID(itemID)
    end
    if GetItemIcon then
        return GetItemIcon(itemID)
    end
    return nil
end

-- 品质、物品等级（用于 Tooltip 里多 ID 数量颜色区分）
local function GetItemQualitySafe(itemID)
    if C_Item and C_Item.GetItemQualityByID then
        local q = C_Item.GetItemQualityByID(itemID)
        if q ~= nil then return q end
    end
    if GetItemInfo then
        return select(3, GetItemInfo(itemID)) or 0
    end
    return 0
end

local function GetItemLevelSafe(itemID)
    if GetItemInfo then
        return select(4, GetItemInfo(itemID)) or 0
    end
    return 0
end

-- Tooltip 右侧数量：高品质暗金、低品质灰白
local COLOR_TIP_COUNT_HIGH = {0.72, 0.54, 0.18}
local COLOR_TIP_COUNT_LOW  = {0.78, 0.78, 0.82}

-- 在拍卖行的搜索框里填入物品名并触发搜索
local function SearchItem(itemName)
    local ah = AuctionHouseFrame or AuctionFrame
    if not ah or not ah:IsShown() then return end

    if ah.SearchBar and ah.SearchBar.SearchBox then
        ah.SearchBar.SearchBox:SetText(itemName)
        if ah.SearchBar.Search then
            pcall(function() ah.SearchBar:Search() end)
        elseif ah.SearchBar.SearchButton then
            pcall(function() ah.SearchBar.SearchButton:Click() end)
        end
    end
end

-- ==========================================
-- 主框架（使用 ButtonFrameTemplate：自带标题、关闭按钮、肖像、嵌入面板）
-- ==========================================
local frame = CreateFrame("Frame", "QuickAuctionBuyerFrame", UIParent, "ButtonFrameTemplate")
frame:SetSize(WINDOW_WIDTH, WINDOW_HEIGHT)
frame:SetPoint("CENTER")
-- 与拍卖行同层（默认 MEDIUM；打开时若 AuctionHouseFrame 已存在则与其 Strata 对齐）
frame:SetFrameStrata("MEDIUM")
frame:HookScript("OnShow", function(self)
    if AuctionHouseFrame and AuctionHouseFrame.GetFrameStrata then
        local s = AuctionHouseFrame:GetFrameStrata()
        if s and s ~= "" then
            self:SetFrameStrata(s)
        end
    end
end)
frame:Hide()
frame:SetClampedToScreen(true)
-- 与同 Strata 窗口叠放时不强行压顶；用户点击本窗口时再升到该层最前（与拍卖行一致）
if frame.SetToplevel then
    frame:SetToplevel(true)
end

local QAB_ITEM_NAME_LOADING = (type(RETRIEVING_ITEM_INFO) == "string" and RETRIEVING_ITEM_INFO ~= "")
    and RETRIEVING_ITEM_INFO or "读取中…"

local function QAB_GetItemNameCached(itemID)
    if not itemID then
        return nil
    end
    if C_Item and C_Item.GetItemInfo then
        local nm = select(1, C_Item.GetItemInfo(itemID))
        if type(nm) == "string" and nm ~= "" then
            return nm
        end
    end
    return nil
end

local function QAB_ScheduleProfessionsRefreshOnItemData()
    if qabProfItemRefreshTimer then
        qabProfItemRefreshTimer:Cancel()
    end
    if C_Timer and C_Timer.NewTimer then
        qabProfItemRefreshTimer = C_Timer.NewTimer(0.05, function()
            qabProfItemRefreshTimer = nil
            if frame:IsShown() and RefreshHomeProfessionsSection then
                RefreshHomeProfessionsSection()
            end
        end)
    elseif RefreshHomeProfessionsSection then
        RefreshHomeProfessionsSection()
    end
end

local function QAB_RequestLoadItemNameForTrack(itemID)
    if not itemID or not C_Item or not C_Item.RequestLoadItemDataByID then
        return
    end
    if QAB_GetItemNameCached(itemID) then
        qabPendingTrackItemIDs[itemID] = nil
        return
    end
    qabPendingTrackItemIDs[itemID] = true
    pcall(C_Item.RequestLoadItemDataByID, itemID)
end

-- requestAsync：为 true 时请求服务器/客户端加载物品数据，收到 ITEM_DATA_LOAD_RESULT 后刷新首页追踪区以显示真名。
GetItemNameSafe = function(itemID, requestAsync)
    local nm = QAB_GetItemNameCached(itemID)
    if nm then
        return nm
    end
    if requestAsync then
        QAB_RequestLoadItemNameForTrack(itemID)
    end
    return QAB_ITEM_NAME_LOADING
end

-- 拖拽
frame:SetMovable(true)
frame:EnableMouse(true)
frame:RegisterForDrag("LeftButton")
frame:SetScript("OnDragStart", frame.StartMoving)
frame:SetScript("OnDragStop", frame.StopMovingOrSizing)

-- 标题（兼容新旧版本：现代 WoW 用 SetTitle，旧版直接写 TitleText）
if frame.SetTitle then
    frame:SetTitle("购物助手")
elseif frame.TitleText then
    frame.TitleText:SetText("购物助手")
elseif _G[frame:GetName() .. "TitleText"] then
    _G[frame:GetName() .. "TitleText"]:SetText("购物助手")
end

-- ==========================================
-- 彻底移除左上角肖像 + 金色圆环
-- Blizzard 官方 API（与 Auctionator / 原生面板一致），会同时处理圆环与布局；
-- 仅靠手动 SetTexture(nil) 无法清掉部分无名称的 ring 纹理。
-- ==========================================
local function ScrubElement(r)
    if not r then return end
    if r.SetAlpha then r:SetAlpha(0) end
    if r.SetTexture then pcall(r.SetTexture, r, nil) end
    if r.Hide then r:Hide() end
end

if ButtonFrameTemplate_HidePortrait then
    ButtonFrameTemplate_HidePortrait(frame)
end
if frame.PortraitContainer then
    frame.PortraitContainer:Hide()
    -- 个别版本圆环在子 Frame 上，递归清纹理
    local function ScrubPortraitSubtree(f)
        if not f then return end
        for i = 1, f:GetNumRegions() do
            ScrubElement(select(i, f:GetRegions()))
        end
        for i = 1, f:GetNumChildren() do
            ScrubPortraitSubtree(select(i, f:GetChildren()))
        end
    end
    ScrubPortraitSubtree(frame.PortraitContainer)
end

-- 兜底：仍有个别版本残留时，穷举已知字段 + 按名字扫描 region
for _, name in ipairs({
    "PortraitFrame", "portraitFrame", "PortraitFrameBg", "Portrait", "portrait",
}) do
    ScrubElement(frame[name])
end
for i = 1, frame:GetNumRegions() do
    local region = select(i, frame:GetRegions())
    local rname = region.GetName and region:GetName()
    if rname and rname:lower():find("portrait") then
        ScrubElement(region)
    end
end

-- 标题：无肖像后居中顶栏
local titleText = frame.TitleText
    or (frame.TitleContainer and frame.TitleContainer.TitleText)
    or _G[(frame:GetName() or "") .. "TitleText"]
if titleText then
    titleText:ClearAllPoints()
    titleText:SetPoint("TOP", frame, "TOP", 0, -6)
    FontDownOne(titleText)
end

-- Inset：与外层金属框四边等距（避免只有左右有缝、顶底贴死）
if frame.Inset then
    frame.Inset:ClearAllPoints()
    -- TOPLEFT第一个参数为左偏移，第二个参数为上偏移，负数是向下正数是向上
    frame.Inset:SetPoint("TOPLEFT", frame, "TOPLEFT", FRAME_INSET_MARGIN, (-FRAME_INSET_MARGIN*2)-5)
    frame.Inset:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -FRAME_INSET_MARGIN+4, FRAME_INSET_MARGIN-2)
end

-- ==========================================
-- 按钮状态管理
-- ==========================================
local allButtons = {}
-- 首页「快速补货」图标单独列表（不计入 allButtons，由 RefreshHomeRestock 维护）
local homeRestockButtons = {}
local homeRestockArea, homeProfArea, homeProfEmptyHint
local homeTabContentFrame
-- 前向声明：在 CreateIconButton 定义之后再赋值，否则会去查全局 CreateIconButton（nil）
local RefreshHomeRestockSection

local function UpdateButtonState(btn)
    if not btn or not btn.itemIDs then return end

    local total = 0
    for _, itemID in ipairs(btn.itemIDs) do
        total = total + GetItemCountSafe(itemID)
    end

    if total > 0 then
        btn.icon:SetDesaturated(false)
        btn.icon:SetVertexColor(1, 1, 1, 1)
        btn.label:SetTextColor(unpack(COLOR_TEXT_LIGHT))
        btn.Count:SetText(total)
        btn.Count:SetTextColor(unpack(COLOR_COUNT))
        btn.Count:Show()
    else
        btn.icon:SetDesaturated(true)
        btn.icon:SetVertexColor(0.6, 0.6, 0.6, 1)
        btn.label:SetTextColor(unpack(COLOR_TEXT_DIM))
        btn.Count:Hide()
    end
end

local function RefreshAllButtons()
    for _, btn in ipairs(allButtons) do
        UpdateButtonState(btn)
    end
    if RefreshHomeRestockSection then
        RefreshHomeRestockSection()
    end
    if RefreshHomeProfessionsSection then
        RefreshHomeProfessionsSection()
    end
end

local function FlattenCatalogBtnData()
    local list = {}
    for _, tabKey in ipairs({"consumable", "enchant", "gem"}) do
        local sections = categoryData[tabKey]
        if sections then
            for _, section in ipairs(sections) do
                for _, btnData in ipairs(section.buttons) do
                    table.insert(list, btnData)
                end
            end
        end
    end
    return list
end

local function BtnDataTotalCount(btnData)
    local ids = {}
    if type(btnData.id) == "table" then
        ids = btnData.id
    elseif btnData.id then
        ids = {btnData.id}
    end
    local total = 0
    for _, itemID in ipairs(ids) do
        total = total + GetItemCountSafe(itemID)
    end
    return total
end

local function tIndexOfQAB(tbl, val)
    for i, v in ipairs(tbl) do
        if v == val then
            return i
        end
    end
    return nil
end

local function QAB_IsBasicReagentSlot(slot)
    if not slot or not slot.reagents or #slot.reagents == 0 then
        return false
    end
    if Enum.CraftingReagentType and Enum.CraftingReagentType.Basic ~= nil then
        return slot.reagentType == Enum.CraftingReagentType.Basic
    end
    return false
end

-- 取消专业技能追踪（10.0.5+ 第三参数 isRecraft）
local function QAB_UntrackRecipe(recipeID, isRecraft)
    if not C_TradeSkillUI or not C_TradeSkillUI.SetRecipeTracked then
        return
    end
    pcall(function()
        C_TradeSkillUI.SetRecipeTracked(recipeID, false, isRecraft and true or false)
    end)
end

-- 取消追踪确认框布局（改数字即可微调）
-- QAB_UNTRACK_POPUP_PAD_X / PAD_Y：正文相对弹窗左右、上侧内边距（像素）；底部留白主要靠文末换行 + 暴雪默认按钮间距
local QAB_UNTRACK_POPUP_PAD_X = 28
local QAB_UNTRACK_POPUP_PAD_Y = 24
-- 弹窗总宽：填 0 表示不覆盖，沿用暴雪 StaticPopup_Resize 结果（通常约 320）；填 380、420 等可整体加宽
local QAB_UNTRACK_POPUP_WIDTH = 0
-- 在暴雪根据文字/按钮算出的高度上再增减（像素）；正数加高、负数变矮，0 表示不改
local QAB_UNTRACK_POPUP_HEIGHT_EXTRA = 50

-- 取消追踪前确认（StaticPopup）
StaticPopupDialogs["QAB_CONFIRM_UNTRACK_RECIPE"] = {
    whileDead = true,
    hideOnEscape = true,
    timeout = 0,
    button1 = YES,
    button2 = NO,
    text = "是否确认取消追踪？",
    OnShow = function(self)
        local anchor = QuickAuctionBuyerFrame or frame
        self:ClearAllPoints()
        if anchor and anchor.IsShown and anchor:IsShown() then
            self:SetPoint("CENTER", anchor, "CENTER", 0, 0)
            if self.SetFrameStrata and anchor.GetFrameStrata then
                local s = anchor:GetFrameStrata()
                if s and s ~= "" then
                    self:SetFrameStrata(s)
                end
            end
            if self.SetFrameLevel and anchor.GetFrameLevel then
                self:SetFrameLevel((anchor:GetFrameLevel() or 0) + 80)
            end
        else
            self:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
        end
        local tf = self.text or (self.GetName and _G[self:GetName() .. "Text"])
        if tf and tf.ClearAllPoints then
            local px, py = QAB_UNTRACK_POPUP_PAD_X, QAB_UNTRACK_POPUP_PAD_Y
            tf:ClearAllPoints()
            tf:SetPoint("TOPLEFT", self, "TOPLEFT", px, -py)
            tf:SetPoint("TOPRIGHT", self, "TOPRIGHT", -px, -py)
        end
        if StaticPopup_Resize then
            StaticPopup_Resize(self, "QAB_CONFIRM_UNTRACK_RECIPE")
        end
        if type(QAB_UNTRACK_POPUP_WIDTH) == "number" and QAB_UNTRACK_POPUP_WIDTH >= 280 then
            self:SetWidth(QAB_UNTRACK_POPUP_WIDTH)
        end
        if type(QAB_UNTRACK_POPUP_HEIGHT_EXTRA) == "number" and QAB_UNTRACK_POPUP_HEIGHT_EXTRA ~= 0 then
            local h = self.GetHeight and self:GetHeight() or 0
            if h > 0 then
                self:SetHeight(math.max(100, h + QAB_UNTRACK_POPUP_HEIGHT_EXTRA))
            end
        end
    end,
    OnAccept = function(self)
        local d = self.data
        if not d or not d.recipeID then
            return
        end
        QAB_UntrackRecipe(d.recipeID, d.isRecraft)
        if RefreshHomeProfessionsSection then
            RefreshHomeProfessionsSection()
        end
        PlaySound(SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON)
    end,
}

-- 追踪行：解析产出物品 ID（与搜索逻辑一致）
local function QAB_GetRecipeOutputItemID(recipeID, recipeSchematic)
    local outputData
    pcall(function()
        outputData = C_TradeSkillUI.GetRecipeOutputItemData(recipeID, {})
    end)
    if outputData and outputData.itemID and outputData.itemID > 0 then
        return outputData.itemID, outputData.hyperlink
    end
    if recipeSchematic and recipeSchematic.outputItemID and recipeSchematic.outputItemID > 0 then
        return recipeSchematic.outputItemID, nil
    end
    if Auctionator and Auctionator.CraftingInfo and Auctionator.CraftingInfo.EnchantSpellsToItems
        and Auctionator.CraftingInfo.EnchantSpellsToItems[recipeID] then
        local ench = Auctionator.CraftingInfo.EnchantSpellsToItems[recipeID][1]
        if ench then
            return ench, nil
        end
    end
    return nil, nil
end

-- 「工程学配方」式副标题：专业名 + 配方类本地化名称
-- isRecraft：与追踪列表一致；用于 GetRecipeInfoForSkillLineAbility 的第二参数（部分配方仅此路径能拿到技能线）。
local function QAB_GetProfessionRecipeSubtitle(recipeID, isRecraft)
    local info = C_TradeSkillUI.GetRecipeInfo(recipeID)
    if not info then
        return ""
    end
    local recipeClass = Enum.ItemClass and Enum.ItemClass.Recipe
    local function professionNameFromLineID(lineID)
        if not lineID or not C_TradeSkillUI.GetProfessionInfoBySkillLineID then
            return nil
        end
        local pi = C_TradeSkillUI.GetProfessionInfoBySkillLineID(lineID)
        if not pi then
            return nil
        end
        return pi.professionName or pi.parentProfessionName or pi.name
    end
    local function lineIDFromRecipeInfo(ri)
        if not ri then
            return nil
        end
        return ri.tradeSkillLineID or ri.skillLineID or ri.currentTradeSkillLineID
    end
    local lineID = lineIDFromRecipeInfo(info)
    -- 追踪场景下 GetRecipeInfo 常不带技能线，但 skillLineAbilityID 可用；用 ForSkillLineAbility 取完整 recipeInfo（见 PatronOffers 等插件用法）。
    if not lineID and info.skillLineAbilityID
        and type(C_TradeSkillUI.GetRecipeInfoForSkillLineAbility) == "function" then
        local flags = {}
        local function addFlag(f)
            for _, x in ipairs(flags) do
                if x == f then
                    return
                end
            end
            flags[#flags + 1] = f
        end
        if isRecraft ~= nil then
            addFlag(isRecraft)
        end
        addFlag(false)
        addFlag(true)
        addFlag(2)
        for _, flag in ipairs(flags) do
            local ok, si = pcall(function()
                return C_TradeSkillUI.GetRecipeInfoForSkillLineAbility(info.skillLineAbilityID, flag)
            end)
            if ok and si then
                lineID = lineIDFromRecipeInfo(si)
                if lineID then
                    break
                end
            end
        end
    end
    local profBase = professionNameFromLineID(lineID)
    if (not profBase or profBase == "") and info.skillLineAbilityID
        and type(C_TradeSkillUI.GetProfessionInfoBySkillLineAbilityID) == "function" then
        local ok, pi = pcall(function()
            return C_TradeSkillUI.GetProfessionInfoBySkillLineAbilityID(info.skillLineAbilityID)
        end)
        if ok and pi then
            profBase = pi.professionName or pi.name
        end
    end
    -- 仅当产出物为「配方」类物品时，子类型常为专业名（如珠宝加工）；成品消耗品无此信息。
    if (not profBase or profBase == "") and recipeClass and C_Item and C_Item.GetItemInfo then
        local outID = select(1, QAB_GetRecipeOutputItemID(recipeID, nil))
        if outID then
            local classID = select(12, C_Item.GetItemInfo(outID))
            local itemSubType = select(7, C_Item.GetItemInfo(outID))
            if classID == recipeClass and type(itemSubType) == "string" and itemSubType ~= "" then
                profBase = itemSubType
            end
        end
    end
    if not profBase or profBase == "" then
        return ""
    end
    local classSuffix = ""
    if recipeClass and C_Item.GetItemClassInfo then
        classSuffix = C_Item.GetItemClassInfo(recipeClass) or ""
    end
    if classSuffix == "" then
        classSuffix = "配方"
    end
    return profBase .. classSuffix
end

-- 专业技能追踪行布局常量
local TRACKED_TRASH_BTN = 18
local TRACKED_ICON_SZ = 38
local TRACKED_HEADER_H = 44
-- 与 iconHolder 的 TOPLEFT x 一致，材料列表左缘与图标区左对齐
local TRACKED_ICON_ROW_INSET_X = 2
-- 标题区（图标+名称+专业）与材料第一行之间额外空隙
local TRACKED_MATERIAL_TOP_GAP = 6
-- 产出图标相对本行 TOPLEFT 的 y；与「快速补货」首行图标 y=0 一致（勿用负值下移，否则会加大与分区标题的间距）
local TRACKED_ROW_ICON_TOP_Y = 0
-- 名称 / 搜索 / 取消 垂直对齐：以产出图标几何中心为基准（与 TRACKED_ROW_ICON_TOP_Y、TRACKED_ICON_SZ 一致）
-- TRACKED_HEADER_V_SHIFT：三条控件整体上移(填正)或下移(填负)，单位像素。
-- TRACKED_NAME_EXTRA_Y：在上一项基础上只挪「物品名」一行（搜索/取消不动），用于字体视觉校正。
local TRACKED_HEADER_V_SHIFT = 0
local TRACKED_NAME_EXTRA_Y = 0
-- 取消按钮距行右缘的内缩（仅水平），一般保持 0
local TRACKED_TRASH_RIGHT_INSET = 0

local function QAB_AtlasExists(atlas)
    return atlas and C_Texture and C_Texture.GetAtlasInfo and C_Texture.GetAtlasInfo(atlas) ~= nil
end

-- 取消追踪按钮：仿天赋「应用改动」旁放弃/撤销键（红圈斜杠 Atlas + 高亮 + 按下位移）
local function QAB_ResetTrashDiscardBtnVisual(self)
    local ic = self and self._qabDiscardIcon
    if not ic then
        return
    end
    ic:ClearAllPoints()
    ic:SetPoint("CENTER", self, "CENTER", 0, 0)
end

local function QAB_CreateTrashTrackButton(parent)
    local btn = CreateFrame("Button", nil, parent)
    btn:SetSize(TRACKED_TRASH_BTN, TRACKED_TRASH_BTN)

    local pad = 2
    local iconSz = math.max(8, TRACKED_TRASH_BTN - pad * 2)
    local icon = btn:CreateTexture(nil, "ARTWORK", nil, 1)
    icon:SetSize(iconSz, iconSz)
    icon:SetPoint("CENTER", btn, "CENTER", 0, 0)
    btn._qabDiscardIcon = icon

    local atlases = {
        "talents-button-discardchanges",
        "talents-button-ignoreChanges",
        "common-icon-nosign",
        "bags-icon-remove",
    }
    local applied
    for _, atlas in ipairs(atlases) do
        if QAB_AtlasExists(atlas) then
            icon:SetAtlas(atlas)
            applied = true
            break
        end
    end
    if not applied then
        icon:SetTexture("Interface\\Buttons\\UI-GroupLoot-Pass-Up")
        icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
    end

    local hl = btn:CreateTexture(nil, "HIGHLIGHT", nil, 2)
    hl:SetAllPoints(icon)
    hl:SetTexture("Interface\\Buttons\\ButtonHilight-Square")
    hl:SetBlendMode("ADD")
    hl:SetAlpha(0.4)

    local pushX, pushY = 1, -1
    btn:RegisterForClicks("LeftButtonUp")
    btn:SetScript("OnMouseDown", function(self, button)
        if button == "LeftButton" and self._qabDiscardIcon then
            self._qabDiscardIcon:ClearAllPoints()
            self._qabDiscardIcon:SetPoint("CENTER", self, "CENTER", pushX, pushY)
        end
    end)
    btn:SetScript("OnMouseUp", function(self, button)
        QAB_ResetTrashDiscardBtnVisual(self)
    end)

    return btn
end

local function QAB_TintSearchButtonRed(btn)
    if not btn then
        return
    end
    for _, key in ipairs({ "Left", "Middle", "Right" }) do
        local r = btn[key]
        if r and r.SetVertexColor then
            r:SetVertexColor(0.68, 0.1, 0.1)
        end
    end
    local fs = btn.GetFontString and btn:GetFontString()
    if fs and fs.SetTextColor then
        fs:SetTextColor(1, 0.9, 0.22)
    end
end

-- 条目底部分割线（纯色近似暴雪列表底栏，不依赖额外素材）
local function QAB_AddTrackedEntryDivider(row)
    local div = row:CreateTexture(nil, "ARTWORK", nil, 2)
    div:SetHeight(1)
    div:SetPoint("BOTTOMLEFT", row, "BOTTOMLEFT", 0, 0)
    div:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", 0, 0)
    -- #929292 中灰，提高与深色背景的对比
    div:SetColorTexture(146 / 255, 146 / 255, 146 / 255, 1)
end

-- 单条追踪配方：按材料批量搜索（与 Auctionator 目标追踪逻辑一致，调用 MultiSearchAdvanced）
local function RunAuctionatorRecipeSearch(recipeID, isRecraft)
    if not (AuctionHouseFrame and AuctionHouseFrame:IsShown()) then
        print("|cffffcc00[购物助手]|r 请先打开拍卖行后再搜索。")
        return
    end
    if not (Auctionator and Auctionator.API and Auctionator.API.v1 and Auctionator.API.v1.MultiSearchAdvanced) then
        print("|cffffcc00[购物助手]|r 需要安装并启用 Auctionator。")
        return
    end
    if not ContinuableContainer or not Item then
        print("|cffffcc00[购物助手]|r 当前客户端无法使用 ContinuableContainer/Item API。")
        return
    end

    local searchTerms = {}
    local possibleItems = {}
    local quantities = {}
    local continuableContainer = ContinuableContainer:Create()

    local outputData = C_TradeSkillUI.GetRecipeOutputItemData(recipeID, {})
    local outputLink = outputData and outputData.hyperlink
    if outputLink then
        table.insert(possibleItems, outputLink)
        continuableContainer:AddContinuable(Item:CreateFromItemLink(outputLink))
    elseif Auctionator.CraftingInfo and Auctionator.CraftingInfo.EnchantSpellsToItems
        and Auctionator.CraftingInfo.EnchantSpellsToItems[recipeID] then
        local itemID = Auctionator.CraftingInfo.EnchantSpellsToItems[recipeID][1]
        table.insert(possibleItems, itemID)
        continuableContainer:AddContinuable(Item:CreateFromItemID(itemID))
    else
        local recipeInfo = C_TradeSkillUI.GetRecipeInfo(recipeID)
        if recipeInfo and recipeInfo.name then
            table.insert(searchTerms, {searchString = recipeInfo.name})
        end
    end
    table.insert(quantities, 0)

    local recipeSchematic = C_TradeSkillUI.GetRecipeSchematic(recipeID, isRecraft)
    if not recipeSchematic or not recipeSchematic.reagentSlotSchematics then
        if #searchTerms > 0 then
            pcall(function()
                Auctionator.API.v1.MultiSearchAdvanced("QuickAuctionBuyer", searchTerms)
            end)
        end
        return
    end

    for _, reagentSlotSchematic in ipairs(recipeSchematic.reagentSlotSchematics) do
        if QAB_IsBasicReagentSlot(reagentSlotSchematic) then
            local itemID = reagentSlotSchematic.reagents[1].itemID
            if itemID ~= nil then
                local index = tIndexOfQAB(possibleItems, itemID)
                if index == nil then
                    continuableContainer:AddContinuable(Item:CreateFromItemID(itemID))
                    table.insert(possibleItems, itemID)
                    table.insert(quantities, reagentSlotSchematic.quantityRequired)
                else
                    quantities[index] = quantities[index] + reagentSlotSchematic.quantityRequired
                end
            end
        end
    end

    local function OnItemInfoReady()
        for index, itemRef in ipairs(possibleItems) do
            local itemInfo = {C_Item.GetItemInfo(itemRef)}
            if itemInfo[1] then
                local bound = false
                if Auctionator.Utilities and Auctionator.Utilities.IsBound then
                    bound = Auctionator.Utilities.IsBound(itemInfo)
                end
                if not bound then
                    table.insert(searchTerms, {
                        searchString = itemInfo[1],
                        isExact = true,
                        quantity = quantities[index],
                    })
                end
            end
        end
        pcall(function()
            Auctionator.API.v1.MultiSearchAdvanced("QuickAuctionBuyer", searchTerms)
        end)
    end

    if #possibleItems == 0 and #searchTerms > 0 then
        pcall(function()
            Auctionator.API.v1.MultiSearchAdvanced("QuickAuctionBuyer", searchTerms)
        end)
        return
    end

    if continuableContainer.ContinueOnLoad then
        continuableContainer:ContinueOnLoad(OnItemInfoReady)
    else
        OnItemInfoReady()
    end
end

local function ClearHomeRestockButtons()
    for _, btn in ipairs(homeRestockButtons) do
        btn:Hide()
        btn:SetParent(nil)
    end
    wipe(homeRestockButtons)
end

local function ClearHomeProfRows()
    if not homeProfArea then
        return
    end
    for i = homeProfArea:GetNumChildren(), 1, -1 do
        local ch = select(i, homeProfArea:GetChildren())
        if ch then
            ch:SetParent(nil)
            ch:Hide()
        end
    end
end

RefreshHomeProfessionsSection = function()
    if not homeProfArea then
        return
    end
    ClearHomeProfRows()
    if homeProfEmptyHint then
        homeProfEmptyHint:Hide()
    end

    if not C_TradeSkillUI or not C_TradeSkillUI.GetRecipesTracked then
        if homeProfEmptyHint and homeTabContentFrame then
            homeProfEmptyHint:SetParent(homeTabContentFrame)
            homeProfEmptyHint:ClearAllPoints()
            homeProfEmptyHint:SetPoint("TOPLEFT", homeProfArea, "TOPLEFT", 0, 0)
            homeProfEmptyHint:SetWidth(CONTENT_WIDTH)
            homeProfEmptyHint:SetJustifyH("LEFT")
            homeProfEmptyHint:SetText("当前版本无法读取追踪配方。")
            homeProfEmptyHint:Show()
        end
        homeProfArea:SetHeight(40)
        if LayoutHomeScroll then
            LayoutHomeScroll()
        end
        return
    end

    local entries = {}
    for _, recipeID in ipairs(C_TradeSkillUI.GetRecipesTracked(true) or {}) do
        table.insert(entries, {recipeID = recipeID, isRecraft = true})
    end
    for _, recipeID in ipairs(C_TradeSkillUI.GetRecipesTracked(false) or {}) do
        table.insert(entries, {recipeID = recipeID, isRecraft = false})
    end

    if #entries == 0 then
        if homeProfEmptyHint and homeTabContentFrame then
            homeProfEmptyHint:SetParent(homeTabContentFrame)
            homeProfEmptyHint:ClearAllPoints()
            homeProfEmptyHint:SetPoint("TOPLEFT", homeProfArea, "TOPLEFT", 0, 0)
            homeProfEmptyHint:SetWidth(CONTENT_WIDTH)
            homeProfEmptyHint:SetJustifyH("LEFT")
            homeProfEmptyHint:SetText("暂无追踪配方（请在专业技能窗口将配方加入追踪）。")
            homeProfEmptyHint:Show()
        end
        homeProfArea:SetHeight(40)
        if LayoutHomeScroll then
            LayoutHomeScroll()
        end
        return
    end

    local yAcc = 0
    for _, entry in ipairs(entries) do
        local recipeID = entry.recipeID
        local isRecraft = entry.isRecraft
        local recipeInfo = C_TradeSkillUI.GetRecipeInfo(recipeID)
        local titleText = (recipeInfo and recipeInfo.name) or ("配方 " .. tostring(recipeID))

        local row = CreateFrame("Frame", nil, homeProfArea)
        row:SetWidth(CONTENT_WIDTH)

        local recipeSchematic = C_TradeSkillUI.GetRecipeSchematic(recipeID, isRecraft)

        local btnWSearch = 70
        local btnGap = 6
        local rightStrip = btnWSearch + btnGap + TRACKED_TRASH_BTN + 10
        -- 与产出图标垂直中心对齐（见上方 TRACKED_HEADER_V_SHIFT / TRACKED_NAME_EXTRA_Y）
        local vAlign = TRACKED_ROW_ICON_TOP_Y - (TRACKED_ICON_SZ / 2) + TRACKED_HEADER_V_SHIFT

        local trashBtn = QAB_CreateTrashTrackButton(row)
        trashBtn:SetPoint(
            "CENTER",
            row,
            "TOPRIGHT",
            -(TRACKED_TRASH_BTN / 2) - TRACKED_TRASH_RIGHT_INSET,
            vAlign
        )
        trashBtn:SetFrameLevel((row:GetFrameLevel() or 0) + 5)

        -- 与拍卖行「搜索」等一致：MagicButtonTemplate（红底 + 金属边 + 默认高亮字色）；失败时退回 UIPanelButtonTemplate + 染色。
        local searchBtn
        local searchUsesMagic = false
        do
            local ok, f = pcall(function()
                return CreateFrame("Button", nil, row, "MagicButtonTemplate")
            end)
            if ok and f then
                searchBtn = f
                searchUsesMagic = true
                searchBtn:SetSize(btnWSearch, 24)
                if MagicButton_OnLoad then
                    pcall(MagicButton_OnLoad, searchBtn)
                end
            else
                searchBtn = CreateFrame("Button", nil, row, "UIPanelButtonTemplate")
                searchBtn:SetSize(btnWSearch, 28)
            end
        end
        searchBtn:SetPoint(
            "CENTER",
            row,
            "TOPRIGHT",
            -(TRACKED_TRASH_BTN + btnGap + btnWSearch / 2) - TRACKED_TRASH_RIGHT_INSET,
            vAlign
        )
        searchBtn:SetFrameLevel((row:GetFrameLevel() or 0) + 5)
        searchBtn:SetText("搜索")
        local stSearch = searchBtn:GetFontString()
        if stSearch and not searchUsesMagic then
            FontDownOne(stSearch)
        end
        if not searchUsesMagic then
            QAB_TintSearchButtonRed(searchBtn)
        end
        searchBtn:SetScript("OnClick", function()
            RunAuctionatorRecipeSearch(recipeID, isRecraft)
            PlaySound(SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON)
        end)
        searchBtn:SetScript("OnEnter", function(self)
            GameTooltip:SetOwner(self, "ANCHOR_LEFT")
            GameTooltip:SetText("按材料批量搜索", 1, 1, 1)
            GameTooltip:AddLine("使用 Auctionator 购物清单（临时列表）在拍卖行中依次搜索本配方材料。", 0.7, 0.7, 0.7, true)
            GameTooltip:Show()
        end)
        searchBtn:SetScript("OnLeave", GameTooltip_Hide)

        -- 产出物：图标（白细边）+ 名称（品质色）+ 专业（灰），与示意图一致排在左侧
        local outputItemID, outputHyperlink = QAB_GetRecipeOutputItemID(recipeID, recipeSchematic)
        local dispName = titleText
        local dispQuality = 1
        local dispTexture
        local dispLink = outputHyperlink

        if outputItemID then
            local nm, link, ql, _, _, _, _, _, _, tex = C_Item.GetItemInfo(outputItemID)
            if type(nm) == "string" and nm ~= "" then
                dispName = nm
            end
            if type(ql) == "number" then
                dispQuality = ql
            elseif recipeInfo and type(recipeInfo.quality) == "number" then
                dispQuality = recipeInfo.quality
            end
            dispTexture = tex
            if (not dispLink or dispLink == "") and type(link) == "string" and link ~= "" then
                dispLink = link
            end
        end
        if (not dispTexture or dispTexture == "") and recipeInfo and recipeInfo.icon then
            dispTexture = recipeInfo.icon
        end
        if recipeInfo and type(recipeInfo.quality) == "number" and not outputItemID then
            dispQuality = recipeInfo.quality
        end

        local confirmName = tostring(dispName or titleText):gsub("%%", "%%%%")
        trashBtn:SetScript("OnClick", function()
            StaticPopup_Show(
                "QAB_CONFIRM_UNTRACK_RECIPE",
                confirmName,
                nil,
                { recipeID = recipeID, isRecraft = isRecraft }
            )
        end)
        trashBtn:SetScript("OnEnter", function(self)
            GameTooltip:SetOwner(self, "ANCHOR_LEFT")
            GameTooltip:SetText("取消追踪", 1, 1, 1)
            GameTooltip:AddLine("从专业技能追踪列表中移除此配方。", 0.7, 0.7, 0.7, true)
            GameTooltip:Show()
        end)
        trashBtn:SetScript("OnLeave", function(self)
            QAB_ResetTrashDiscardBtnVisual(self)
            GameTooltip_Hide()
        end)

        local qn = dispQuality
        if type(qn) ~= "number" or qn < 0 or qn > 8 then
            qn = 1
        end
        local rQ, gQ, bQ = 1, 1, 1
        if C_Item and C_Item.GetItemQualityColor then
            rQ, gQ, bQ = C_Item.GetItemQualityColor(qn)
        elseif GetItemQualityColor then
            rQ, gQ, bQ = GetItemQualityColor(qn)
        end

        -- 产出图标：与「快速补货」相同（黑底 + 细边 + 悬停高亮 + 右下角数量），提示锚点 ANCHOR_RIGHT
        local iconColW = TRACKED_ICON_SZ + 4
        local trackIconData = {
            professionTrackOutput = true,
            iconSize = TRACKED_ICON_SZ,
            search = "",
            label = "",
        }
        if outputItemID then
            trackIconData.id = outputItemID
        end
        if dispTexture and dispTexture ~= 0 then
            trackIconData.fallbackIconTexture = dispTexture
        end
        local outIconBtn = CreateIconButton(row, trackIconData, false)
        outIconBtn:SetPoint("TOPLEFT", row, "TOPLEFT", TRACKED_ICON_ROW_INSET_X, TRACKED_ROW_ICON_TOP_Y)
        outIconBtn:SetFrameLevel((row:GetFrameLevel() or 0) + 4)
        outIconBtn:SetScript("OnEnter", function(self)
            GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
            if dispLink then
                GameTooltip:SetHyperlink(dispLink)
            elseif outputItemID then
                GameTooltip:SetItemByID(outputItemID)
            else
                GameTooltip:SetText(dispName, 1, 1, 1)
            end
            GameTooltip:Show()
        end)
        outIconBtn:RegisterForClicks("LeftButtonUp")
        outIconBtn:SetScript("OnClick", function()
            local ah = AuctionHouseFrame or AuctionFrame
            if not ah or not ah:IsShown() then
                print("|cffffcc00[购物助手]|r 请先打开拍卖行后再搜索。")
                return
            end
            local q = dispName
            if type(q) ~= "string" or q == "" then
                q = titleText
            end
            if type(q) == "string" and q ~= "" then
                SearchItem(q)
                PlaySound(SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON)
            end
        end)

        local textW = math.max(80, CONTENT_WIDTH - iconColW - rightStrip - 14)
        local outName = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        -- LEFT↔RIGHT 垂直以图标右侧中点为基准，与 vAlign 同一套偏移，便于与搜索/取消共线
        outName:SetPoint("LEFT", outIconBtn, "RIGHT", 8, TRACKED_HEADER_V_SHIFT + TRACKED_NAME_EXTRA_Y)
        outName:SetWidth(textW)
        outName:SetJustifyH("LEFT")
        if outName.SetNonSpaceWrap then
            outName:SetNonSpaceWrap(false)
        end
        if outName.SetMaxLines then
            outName:SetMaxLines(2)
        end
        outName:SetWordWrap(true)
        outName:SetText(dispName)
        outName:SetTextColor(rQ, gQ, bQ)
        FontDownOne(outName)

        local profStr = QAB_GetProfessionRecipeSubtitle(recipeID, isRecraft)
        local outProf = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        outProf:SetPoint("TOPLEFT", outName, "BOTTOMLEFT", 0, -4)
        outProf:SetWidth(textW)
        outProf:SetJustifyH("LEFT")
        outProf:SetText(profStr ~= "" and profStr or " ")
        outProf:SetTextColor(unpack(COLOR_TRACKED_PROF_SUBTITLE))
        FontDownOne(outProf)

        local lineY = -(TRACKED_HEADER_H + TRACKED_MATERIAL_TOP_GAP)
        if recipeSchematic and recipeSchematic.reagentSlotSchematics then
            for _, slot in ipairs(recipeSchematic.reagentSlotSchematics) do
                if QAB_IsBasicReagentSlot(slot) then
                    local itemID = slot.reagents[1].itemID
                    if itemID then
                        local req = slot.quantityRequired or 0
                        local have = GetItemCountSafe(itemID)
                        local nm = GetItemNameSafe(itemID, true)
                        local mat = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
                        mat:SetPoint("TOPLEFT", row, "TOPLEFT", TRACKED_ICON_ROW_INSET_X, lineY)
                        mat:SetWidth(math.max(60, CONTENT_WIDTH - TRACKED_ICON_ROW_INSET_X - rightStrip - 10))
                        mat:SetJustifyH("LEFT")
                        mat:SetText(string.format("%d/%d  %s", have, req, nm))
                        FontDownOne(mat)
                        if have >= req then
                            mat:SetTextColor(unpack(COLOR_TEXT_LIGHT))
                        else
                            mat:SetTextColor(unpack(COLOR_TEXT_DIM))
                        end
                        lineY = lineY - 14
                    end
                end
            end
        end

        QAB_AddTrackedEntryDivider(row)

        local rowH = math.max(TRACKED_HEADER_H + TRACKED_MATERIAL_TOP_GAP + 6, math.abs(lineY) + 10)
        row:SetHeight(rowH)
        row:SetPoint("TOPLEFT", homeProfArea, "TOPLEFT", 0, -yAcc)
        yAcc = yAcc + rowH + 10
    end

    homeProfArea:SetHeight(math.max(40, yAcc))
    if LayoutHomeScroll then
        LayoutHomeScroll()
    end
end

-- ==========================================
-- 创建一个图标按钮（手工组装，使用原生纹理与字体对象）
-- 结构：
--   底层暗色衬底 → 图标 → 高亮纹理（OVERLAY ADD） → 按下纹理
--   外圈 1px 暗色细边（让相邻图标视觉分离）
--   右下角数量文字（NumberFontNormal）
--   下方文字标签（GameFontHighlightSmall）
-- ==========================================
CreateIconButton = function(parent, btnData, registerInAllButtons)
    if registerInAllButtons == nil then
        registerInAllButtons = true
    end
    local btn = CreateFrame("Button", nil, parent)
    local sz = btnData.iconSize or ICON_SIZE
    btn:SetSize(sz, sz)

    -- 整理 itemIDs
    if type(btnData.id) == "table" then
        btn.itemIDs = btnData.id
    elseif btnData.id then
        btn.itemIDs = {btnData.id}
    else
        btn.itemIDs = {}
    end
    btn.searchName = btnData.search

    -- 取第一个有效图标路径
    local iconPath = "Interface\\Icons\\INV_Misc_QuestionMark"
    for _, id in ipairs(btn.itemIDs) do
        local p = GetItemIconSafe(id)
        if p then iconPath = p; break end
    end
    if btnData.fallbackIconTexture and btnData.fallbackIconTexture ~= 0
        and iconPath == "Interface\\Icons\\INV_Misc_QuestionMark" then
        iconPath = btnData.fallbackIconTexture
    end

    -- 暗色衬底（防止图标出现镂空）
    local bg = btn:CreateTexture(nil, "BACKGROUND")
    bg:SetAllPoints()
    bg:SetColorTexture(0, 0, 0, 1)

    -- 图标本体
    local icon = btn:CreateTexture(nil, "ARTWORK")
    icon:SetAllPoints()
    icon:SetTexture(iconPath)
    icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)   -- 裁掉 Blizzard 图标固有的圆角
    btn.icon = icon

    -- 1px 暗色细边（4 条 ColorTexture）
    local function MakeEdge()
        local t = btn:CreateTexture(nil, "BORDER")
        t:SetColorTexture(0, 0, 0, 0.9)
        return t
    end
    local eT, eB, eL, eR = MakeEdge(), MakeEdge(), MakeEdge(), MakeEdge()
    eT:SetPoint("TOPLEFT");     eT:SetPoint("TOPRIGHT");    eT:SetHeight(1)
    eB:SetPoint("BOTTOMLEFT");  eB:SetPoint("BOTTOMRIGHT"); eB:SetHeight(1)
    eL:SetPoint("TOPLEFT");     eL:SetPoint("BOTTOMLEFT");  eL:SetWidth(1)
    eR:SetPoint("TOPRIGHT");    eR:SetPoint("BOTTOMRIGHT"); eR:SetWidth(1)

    -- 鼠标悬停高亮（原生白色高亮纹理 + ADD 混合）
    btn:SetHighlightTexture("Interface\\Buttons\\ButtonHilight-Square", "ADD")
    local hl = btn:GetHighlightTexture()
    if hl then hl:SetAllPoints() end

    -- 按下视觉
    btn:SetPushedTexture("Interface\\Buttons\\UI-Quickslot-Depress")
    local pushed = btn:GetPushedTexture()
    if pushed then pushed:SetAllPoints() end

    -- 数量文字（右下角）
    local count = btn:CreateFontString(nil, "OVERLAY", "NumberFontNormal")
    count:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -1, 1)
    count:Hide()
    FontDownOne(count)
    btn.Count = count

    -- 图标下方的标签
    local label = btn:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    label:SetPoint("TOP", btn, "BOTTOM", 0, -LABEL_GAP)
    label:SetText(btnData.label or "")
    label:SetTextColor(unpack(COLOR_TEXT_LIGHT))
    FontDownOne(label)
    btn.label = label

    if btnData.professionTrackOutput then
        btn.label:Hide()
        btn:RegisterForClicks()
        btn:SetScript("OnClick", nil)
        btn:SetScript("OnEnter", function() end)
        btn:SetScript("OnLeave", GameTooltip_Hide)
    else
        -- 点击：在拍卖行搜索
        btn:RegisterForClicks("LeftButtonUp")
        btn:SetScript("OnClick", function()
            SearchItem(btnData.search)
            PlaySound(SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON)
        end)

        -- 鼠标悬停：显示物品名 + 各 ID 在背包数量（与拍卖行侧一致 ANCHOR_RIGHT）
        btn:HookScript("OnEnter", function(self)
            GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
            GameTooltip:SetText(btnData.search, 1, 1, 1)
            GameTooltip:AddLine("点击在拍卖行搜索此物品", 0.7, 0.7, 0.7)

            if #self.itemIDs > 1 then
                local rows = {}
                for _, id in ipairs(self.itemIDs) do
                    local c = GetItemCountSafe(id)
                    if c > 0 then
                        table.insert(rows, {
                            name    = GetItemNameSafe(id, true),
                            count   = c,
                            quality = GetItemQualitySafe(id),
                            ilvl    = GetItemLevelSafe(id),
                        })
                    end
                end
                if #rows > 0 then
                    GameTooltip:AddLine(" ")
                    local bestIdx = 1
                    if #rows > 1 then
                        for i = 2, #rows do
                            if rows[i].quality > rows[bestIdx].quality then
                                bestIdx = i
                            elseif rows[i].quality == rows[bestIdx].quality
                                and rows[i].ilvl > rows[bestIdx].ilvl then
                                bestIdx = i
                            end
                        end
                    end
                    for i, r in ipairs(rows) do
                        local cr, cg, cb
                        if #rows == 1 then
                            if r.quality >= 3 then
                                cr, cg, cb = unpack(COLOR_TIP_COUNT_HIGH)
                            else
                                cr, cg, cb = unpack(COLOR_TIP_COUNT_LOW)
                            end
                        else
                            if i == bestIdx then
                                cr, cg, cb = unpack(COLOR_TIP_COUNT_HIGH)
                            else
                                cr, cg, cb = unpack(COLOR_TIP_COUNT_LOW)
                            end
                        end
                        GameTooltip:AddDoubleLine(r.name, tostring(r.count), cr, cg, cb, cr, cg, cb)
                    end
                end
            end
            GameTooltip:Show()
        end)
        btn:HookScript("OnLeave", GameTooltip_Hide)
    end

    if registerInAllButtons then
        table.insert(allButtons, btn)
    end

    if btnData.professionTrackOutput then
        if #btn.itemIDs > 0 and btn.itemIDs[1] then
            UpdateButtonState(btn)
        elseif btnData.fallbackIconTexture and btnData.fallbackIconTexture ~= 0 then
            btn.icon:SetTexture(btnData.fallbackIconTexture)
            btn.icon:SetDesaturated(false)
            btn.icon:SetVertexColor(1, 1, 1, 1)
            btn.Count:Hide()
        else
            btn.icon:SetColorTexture(0.12, 0.12, 0.12, 1)
            btn.Count:Hide()
        end
    else
        UpdateButtonState(btn)
    end

    return btn
end

RefreshHomeRestockSection = function()
    if not homeRestockArea then
        return
    end
    ClearHomeRestockButtons()

    local rowStartX = 0
    local y = 0
    local iconsPerRow = math.max(1, math.floor((CONTENT_WIDTH + ICON_HGAP) / (ICON_SIZE + ICON_HGAP)))
    local col = 0
    local maxY = 0

    for _, btnData in ipairs(FlattenCatalogBtnData()) do
        if BtnDataTotalCount(btnData) > 0 then
            local btn = CreateIconButton(homeRestockArea, btnData, false)
            table.insert(homeRestockButtons, btn)
            local x = rowStartX + col * (ICON_SIZE + ICON_HGAP)
            btn:SetPoint("TOPLEFT", homeRestockArea, "TOPLEFT", x, y)
            col = col + 1
            if col >= iconsPerRow then
                col = 0
                y = y - (ICON_SIZE + ICON_VGAP)
            end
            maxY = math.max(maxY, math.abs(y) + ICON_SIZE + ICON_VGAP)
        end
    end

    if col > 0 then
        y = y - (ICON_SIZE + ICON_VGAP)
        maxY = math.max(maxY, math.abs(y))
    end

    local h = math.max(40, maxY)
    homeRestockArea:SetHeight(h)
    if homeRestockArea._emptyHint then
        homeRestockArea._emptyHint:Hide()
        if #homeRestockButtons == 0 then
            homeRestockArea._emptyHint:Show()
        end
    end
    if LayoutHomeScroll then
        LayoutHomeScroll()
    end
end

-- ==========================================
-- 子类别标题（金色文字 + 一根细金线分隔）
-- ==========================================
local function CreateSectionHeader(parent, text)
    local f = CreateFrame("Frame", nil, parent)
    f:SetHeight(SECTION_HEADER_H)

    local label = f:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    label:SetPoint("LEFT", f, "LEFT", 0, 0)
    label:SetText(text)
    label:SetTextColor(unpack(COLOR_TEXT_NORMAL))
    FontDownOne(label)

    -- 标题右侧延伸到容器边的金色细分割线（原生 ColorTexture，无外部素材）
    local line = f:CreateTexture(nil, "ARTWORK")
    line:SetHeight(1)
    line:SetPoint("LEFT", label, "RIGHT", 8, 0)
    line:SetPoint("RIGHT", f, "RIGHT", 0, 0)
    line:SetColorTexture(0.6, 0.5, 0.3, 0.6)

    return f
end

-- 首页布局必须在 CreateSectionHeader / CreateIconButton 之后定义（否则运行时会解析为全局 nil）
function LayoutHomeScroll()
    local c = homeTabContentFrame
    if not c or not homeRestockArea or not homeProfArea then
        return
    end
    local h1 = c._homeHdrRestock
    local h2 = c._homeHdrProf
    if h1 then
        h1:ClearAllPoints()
        h1:SetPoint("TOPLEFT", c, "TOPLEFT", 0, 0)
        h1:SetPoint("RIGHT", c, "RIGHT", 0, 0)
    end
    homeRestockArea:ClearAllPoints()
    homeRestockArea:SetPoint("TOPLEFT", h1 or c, "BOTTOMLEFT", 0, h1 and -SECTION_V_SPACING or 0)
    homeRestockArea:SetWidth(CONTENT_WIDTH)
    if h2 then
        h2:ClearAllPoints()
        h2:SetPoint("TOPLEFT", homeRestockArea, "BOTTOMLEFT", 0, -SECTION_V_SPACING)
        h2:SetPoint("RIGHT", c, "RIGHT", 0, 0)
    end
    homeProfArea:ClearAllPoints()
    homeProfArea:SetPoint("TOPLEFT", h2 or homeRestockArea, "BOTTOMLEFT", 0, h2 and -SECTION_V_SPACING or -SECTION_V_SPACING)
    homeProfArea:SetWidth(CONTENT_WIDTH)

    local totalH = (h1 and SECTION_HEADER_H or 0) + SECTION_V_SPACING + homeRestockArea:GetHeight()
        + SECTION_V_SPACING + (h2 and SECTION_HEADER_H or 0) + SECTION_V_SPACING + homeProfArea:GetHeight()
        + CONTENT_MARGIN
    c:SetHeight(math.max(120, totalH))
    local sf = c:GetParent()
    if sf and sf:GetObjectType() == "ScrollFrame" and sf.UpdateScrollChildRect then
        sf:UpdateScrollChildRect()
    end
end

local function BuildHomeTab(content)
    homeTabContentFrame = content
    content._homeHdrRestock = CreateSectionHeader(content, "快速补货")
    content._homeHdrProf = CreateSectionHeader(content, "专业技能追踪")

    homeRestockArea = CreateFrame("Frame", nil, content)
    homeRestockArea:SetWidth(CONTENT_WIDTH)
    homeRestockArea:SetHeight(40)

    local emptyRestock = content:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    emptyRestock:SetParent(homeRestockArea)
    emptyRestock:SetPoint("TOPLEFT", homeRestockArea, "TOPLEFT", 0, 0)
    emptyRestock:SetWidth(CONTENT_WIDTH)
    emptyRestock:SetJustifyH("LEFT")
    emptyRestock:SetTextColor(unpack(COLOR_TEXT_DIM))
    emptyRestock:SetText("背包中暂无本助手所列物品。")
    FontDownOne(emptyRestock)
    emptyRestock:Hide()
    homeRestockArea._emptyHint = emptyRestock

    homeProfArea = CreateFrame("Frame", nil, content)
    homeProfArea:SetWidth(CONTENT_WIDTH)
    homeProfArea:SetHeight(40)

    homeProfEmptyHint = content:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    homeProfEmptyHint:SetTextColor(unpack(COLOR_TEXT_DIM))
    homeProfEmptyHint:SetWidth(CONTENT_WIDTH)
    homeProfEmptyHint:SetJustifyH("LEFT")
    FontDownOne(homeProfEmptyHint)
    homeProfEmptyHint:Hide()

    LayoutHomeScroll()
    RefreshHomeRestockSection()
    RefreshHomeProfessionsSection()
end

-- ==========================================
-- 创建一个 tab 内容滚动区
-- ==========================================
local function CreateScrollContent(parent)
    local sf = CreateFrame("ScrollFrame", nil, parent, "UIPanelScrollFrameTemplate")
    -- 左/上/底：CONTENT_MARGIN；右侧为滚动条留出宽度
    sf:SetPoint("TOPLEFT", parent, "TOPLEFT", CONTENT_MARGIN, -CONTENT_MARGIN)
    sf:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", -SCROLLBAR_WIDTH, CONTENT_MARGIN)

    -- 滚动条：锚在 Tab 容器（parent）最右侧，上/下与内容区顶底对齐顶满（不再相对 ScrollChild 缩进）
    if sf.ScrollBar then
        sf.ScrollBar:ClearAllPoints()
        sf.ScrollBar:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -3, -CONTENT_MARGIN)
        sf.ScrollBar:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", 0, CONTENT_MARGIN-3)
    end

    local content = CreateFrame("Frame", nil, sf)
    sf:SetScrollChild(content)
    content:SetSize(CONTENT_WIDTH, 10)

    return sf, content
end

-- 消耗品 tab：无滚动条；content 左右锚在 holder 上，宽度与深色可视区一致，避免标题横线超出面板
local function CreateStaticCategoryContent(parent)
    local holder = CreateFrame("Frame", nil, parent)
    holder:SetPoint("TOPLEFT", parent, "TOPLEFT", CONTENT_MARGIN, -CONTENT_MARGIN)
    holder:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", -CONTENT_MARGIN, CONTENT_MARGIN)
    local content = CreateFrame("Frame", nil, holder)
    content:SetPoint("TOPLEFT", holder, "TOPLEFT", 0, 0)
    content:SetPoint("TOPRIGHT", holder, "TOPRIGHT", 0, 0)
    content:SetHeight(10)
    return content
end

-- ==========================================
-- 把一个 tab 的所有子类别铺到 content 中
-- ==========================================
local function BuildCategoryContent(content, sections)
    -- 图标从左向右排布，第一列与区标题左缘对齐（标题栏为 TOPLEFT 0，文字贴 content 左缘）
    local rowStartX = 0

    -- 顶边留白仅由 ScrollFrame 的 CONTENT_MARGIN；此处 y 从 0 起
    local y = 0
    local numSections = #sections
    for secIdx, section in ipairs(sections) do
        local header = CreateSectionHeader(content, section.title)
        header:SetPoint("TOPLEFT", content, "TOPLEFT", 0, y)
        header:SetPoint("RIGHT", content, "RIGHT", 0, 0)
        y = y - SECTION_HEADER_H - SECTION_V_SPACING

        for i, btnData in ipairs(section.buttons) do
            local btn = CreateIconButton(content, btnData)
            local x = rowStartX + (i - 1) * (ICON_SIZE + ICON_HGAP)
            btn:SetPoint("TOPLEFT", content, "TOPLEFT", x, y)
        end
        y = y - (ICON_SIZE + ICON_VGAP)

        if secIdx < numSections then
            y = y - SECTION_V_SPACING
        end
    end

    content:SetHeight(math.max(10, math.abs(y) + CONTENT_MARGIN))
end

-- ==========================================
-- 创建"关于"页内容
-- ==========================================
local function BuildAboutContent(parent)
    -- 关于页的 parent 已是 pageHolder（外圈 CONTENT_MARGIN 已在 Build 里处理），此处勿再套一层 CONTENT_MARGIN，否则顶/左边距会双倍于首页、消耗品等页
    local pad = 0

    local title = parent:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    title:SetPoint("TOPLEFT", parent, "TOPLEFT", pad, -pad)
    title:SetText("购物助手  ·  QuickAuctionBuyer")
    title:SetTextColor(unpack(COLOR_TEXT_NORMAL))
    FontDownOne(title)

    local div = parent:CreateTexture(nil, "ARTWORK")
    div:SetHeight(1)
    div:SetPoint("LEFT", parent, "LEFT", pad, 0)
    div:SetPoint("RIGHT", parent, "RIGHT", -pad, 0)
    div:SetPoint("TOP", title, "BOTTOM", 0, -4)
    div:SetColorTexture(0.6, 0.5, 0.3, 0.7)

    local function AddInfoRow(label, value, anchorTo, gap)
        local lbl = parent:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        lbl:SetPoint("TOPLEFT", anchorTo, "BOTTOMLEFT", 0, -(gap or 6))
        lbl:SetText(label)
        lbl:SetTextColor(unpack(COLOR_TEXT_NORMAL))

        local val = parent:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
        val:SetPoint("LEFT", lbl, "RIGHT", 6, 0)
        val:SetText(value)
        FontDownOne(lbl)
        FontDownOne(val)
        return lbl
    end

    local r1 = AddInfoRow("适用版本：", "WoW 正式服 12.x（至暗之夜 / Midnight）", div, 14)
    local r2 = AddInfoRow("开发时间：", "2026 年 4 月",                       r1, 6)
    local r3 = AddInfoRow("开发工具：", "Cursor (Composer-1, Sonnet 4.7)",   r2, 6)

    -- 子段落：致谢
    local function CreateSubHeader(text, anchorTo, gap)
        local f = CreateFrame("Frame", nil, parent)
        f:SetHeight(20)
        f:SetPoint("TOPLEFT", anchorTo, "BOTTOMLEFT", 0, -(gap or 16))
        f:SetPoint("RIGHT", parent, "RIGHT", -pad, 0)

        local lbl = f:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
        lbl:SetPoint("LEFT", f, "LEFT", 0, 0)
        lbl:SetText(text)
        lbl:SetTextColor(unpack(COLOR_TEXT_NORMAL))
        FontDownOne(lbl)

        local line = f:CreateTexture(nil, "ARTWORK")
        line:SetHeight(1)
        line:SetPoint("LEFT", lbl, "RIGHT", 8, 0)
        line:SetPoint("RIGHT", f, "RIGHT", 0, 0)
        line:SetColorTexture(0.6, 0.5, 0.3, 0.6)

        return f
    end

    local h1 = CreateSubHeader("致谢", r3, 18)
    local thanks = parent:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    thanks:SetPoint("TOPLEFT", h1, "BOTTOMLEFT", 0, -8)
    thanks:SetPoint("RIGHT", parent, "RIGHT", -pad, 0)
    thanks:SetJustifyH("LEFT")
    thanks:SetSpacing(4)
    thanks:SetText(
        "本插件基于 NGA 论坛 |cffffd200冰摇柠檬|r 与 |cffffd200雪白的黑牛|r 在 WeakAuras 上的版本重制，"
        .. "原始 WA 作者已无法追溯，在此一并致以衷心感谢。"
    )
    FontDownOne(thanks)

    local h2 = CreateSubHeader("使用提示", thanks, 18)
    local tips = parent:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    tips:SetPoint("TOPLEFT", h2, "BOTTOMLEFT", 0, -8)
    tips:SetPoint("RIGHT", parent, "RIGHT", -pad, 0)
    tips:SetJustifyH("LEFT")
    tips:SetSpacing(4)
    tips:SetText(
        "·  打开拍卖行后窗口自动出现，关闭后自动隐藏\n"
        .. "·  点击图标即可在拍卖行中搜索对应物品\n"
        .. "·  鼠标悬停可查看详情；包内已有数量会显示在右下角\n"
        .. "·  「首页」快速补货列出本助手配置中背包已有的物品；专业技能追踪需 Auctionator，搜索为材料购物清单"
    )
    FontDownOne(tips)
end

-- ==========================================
-- 构建每个 tab 的内容容器（直接挂在 frame.Inset 上）
-- ==========================================
local tabContents = {}     -- tabIndex -> 容器 Frame
-- Inset 父容器（兼容性兜底）
local insetParent = frame.Inset or frame.inset or frame
do
    for i, info in ipairs(TAB_DEFS) do
        local container = CreateFrame("Frame", nil, insetParent)
        container:SetAllPoints(insetParent)
        container:Hide()

        if info.key == "about" then
            -- 关于页直接铺满容器，不需要滚动
            local pageHolder = CreateFrame("Frame", nil, container)
            pageHolder:SetPoint("TOPLEFT", container, "TOPLEFT", CONTENT_MARGIN, -CONTENT_MARGIN)
            pageHolder:SetPoint("BOTTOMRIGHT", container, "BOTTOMRIGHT", -CONTENT_MARGIN, CONTENT_MARGIN)
            BuildAboutContent(pageHolder)
        elseif info.key == "home" then
            local _, content = CreateScrollContent(container)
            BuildHomeTab(content)
        elseif info.key == "consumable" then
            -- 第一个数据 tab：无滚动条，内容区略宽（原滚动条宽度）
            local content = CreateStaticCategoryContent(container)
            BuildCategoryContent(content, categoryData[info.key])
        else
            local _, content = CreateScrollContent(container)
            BuildCategoryContent(content, categoryData[info.key])
        end

        tabContents[i] = container
    end
end

-- ==========================================
-- 底部 Tab 按钮（原生 PanelTabButtonTemplate）
-- ==========================================
local function ShowTab(tabID)
    PanelTemplates_SetTab(frame, tabID)
    for i, c in ipairs(tabContents) do
        if i == tabID then c:Show() else c:Hide() end
    end
    if TAB_DEFS[tabID] and TAB_DEFS[tabID].key == "home" then
        if RefreshHomeProfessionsSection then
            RefreshHomeProfessionsSection()
        end
        if RefreshHomeRestockSection then
            RefreshHomeRestockSection()
        end
    end
    PlaySound(SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON)
end

local prevTab
for i, info in ipairs(TAB_DEFS) do
    local tab = CreateFrame("Button", "QuickAuctionBuyerFrameTab" .. i, frame, "PanelTabButtonTemplate")
    tab:SetID(i)
    tab:SetText(info.name)
    -- PanelTemplates_TabResize(tab, padding, absoluteSize, minWidth, maxWidth, absoluteTextSize)
    -- padding=24 给中文留够呼吸空间；minWidth=64 防止短词标签太挤
    PanelTemplates_TabResize(tab, 24, nil, 64)
    tab:SetScript("OnClick", function(self)
        ShowTab(self:GetID())
    end)
    local tabFs = tab.Text or tab.FontString
    if tabFs then FontDownOne(tabFs) end

    if i == 1 then
        tab:SetPoint("TOPLEFT", frame, "BOTTOMLEFT", TAB_BAR_LEFT_OFFSET, 2)
    else
        -- PanelTabButtonTemplate 自带 ~16px 透明端帽，所以用 +4 让相邻 tab 视觉间隔约 4px
        tab:SetPoint("LEFT", prevTab, "RIGHT", 4, 0)
    end
    prevTab = tab
end

frame.numTabs = #TAB_DEFS
ShowTab(1)

-- ==========================================
-- 最小化图标（关闭主框架后停在拍卖行右侧）
-- ==========================================
local minimizeIcon = CreateFrame("Button", "QuickAuctionBuyerMinimizeIcon", UIParent)
minimizeIcon:SetSize(40, 40)
minimizeIcon:SetFrameStrata("MEDIUM")
if minimizeIcon.SetToplevel then
    minimizeIcon:SetToplevel(true)
end
minimizeIcon:Hide()

local miBg = minimizeIcon:CreateTexture(nil, "BACKGROUND")
miBg:SetAllPoints()
miBg:SetColorTexture(0, 0, 0, 1)

local miIcon = minimizeIcon:CreateTexture(nil, "ARTWORK")
miIcon:SetPoint("TOPLEFT", 1, -1)
miIcon:SetPoint("BOTTOMRIGHT", -1, 1)
miIcon:SetTexture("Interface\\Icons\\INV_Misc_Coin_01")
miIcon:SetTexCoord(0.08, 0.92, 0.08, 0.92)

-- 1px 暗色细边
for _, side in ipairs({"TOP", "BOTTOM", "LEFT", "RIGHT"}) do
    local t = minimizeIcon:CreateTexture(nil, "BORDER")
    t:SetColorTexture(0, 0, 0, 1)
    if side == "TOP" or side == "BOTTOM" then
        t:SetHeight(1)
        t:SetPoint(side .. "LEFT")
        t:SetPoint(side .. "RIGHT")
    else
        t:SetWidth(1)
        t:SetPoint("TOP" .. side)
        t:SetPoint("BOTTOM" .. side)
    end
end

minimizeIcon:SetHighlightTexture("Interface\\Buttons\\ButtonHilight-Square", "ADD")
local miHL = minimizeIcon:GetHighlightTexture()
if miHL then miHL:SetAllPoints() end

minimizeIcon:HookScript("OnEnter", function(self)
    GameTooltip:SetOwner(self, "ANCHOR_LEFT")
    GameTooltip:SetText("购物助手", 1, 1, 1)
    GameTooltip:AddLine("点击展开", 0.7, 0.7, 0.7)
    GameTooltip:Show()
end)
minimizeIcon:HookScript("OnLeave", GameTooltip_Hide)

-- ==========================================
-- 拍卖行联动 / 事件
-- ==========================================
local isMinimized = false

local function PositionByAuctionHouse()
    local ah = AuctionHouseFrame or AuctionFrame
    if not ah then return end
    frame:ClearAllPoints()
    frame:SetPoint("TOPLEFT", ah, "TOPRIGHT", 5, 0)
    minimizeIcon:ClearAllPoints()
    minimizeIcon:SetPoint("TOPLEFT", ah, "TOPRIGHT", 5, -2)
end

local function ShowMain()
    isMinimized = false
    minimizeIcon:Hide()
    PositionByAuctionHouse()
    frame:Show()
end

local function ShowMinimized()
    isMinimized = true
    frame:Hide()
    PositionByAuctionHouse()
    minimizeIcon:Show()
end

local function HideAll()
    frame:Hide()
    minimizeIcon:Hide()
end

-- 关闭按钮：缩小成图标而不是真正关闭
if frame.CloseButton then
    frame.CloseButton:SetScript("OnClick", function()
        ShowMinimized()
        PlaySound(SOUNDKIT.IG_MAINMENU_CLOSE)
    end)
end

minimizeIcon:SetScript("OnClick", function()
    ShowMain()
    PlaySound(SOUNDKIT.IG_MAINMENU_OPEN)
end)

frame:RegisterEvent("AUCTION_HOUSE_SHOW")
frame:RegisterEvent("AUCTION_HOUSE_CLOSED")
frame:RegisterEvent("BAG_UPDATE")
frame:RegisterEvent("PLAYER_LOGIN")
pcall(function()
    frame:RegisterEvent("TRACKED_RECIPE_UPDATE")
end)
frame:RegisterEvent("ITEM_DATA_LOAD_RESULT")

frame:SetScript("OnEvent", function(self, event, ...)
    if event == "AUCTION_HOUSE_SHOW" then
        if isMinimized then
            ShowMinimized()
        else
            ShowMain()
        end
        RefreshAllButtons()
    elseif event == "AUCTION_HOUSE_CLOSED" then
        HideAll()
    elseif event == "BAG_UPDATE" then
        RefreshAllButtons()
    elseif event == "PLAYER_LOGIN" then
        RefreshAllButtons()
    elseif event == "TRACKED_RECIPE_UPDATE" then
        if RefreshHomeProfessionsSection then
            RefreshHomeProfessionsSection()
        end
    elseif event == "ITEM_DATA_LOAD_RESULT" then
        local itemIDLoaded, success = ...
        if itemIDLoaded and qabPendingTrackItemIDs[itemIDLoaded] then
            qabPendingTrackItemIDs[itemIDLoaded] = nil
            if success then
                QAB_ScheduleProfessionsRefreshOnItemData()
            end
        end
    end
end)

-- 实时跟踪：拍卖行存在但插件被隐藏过时自动恢复，拍卖行关闭后兜底
local watcher = CreateFrame("Frame")
watcher.elapsed = 0
watcher:SetScript("OnUpdate", function(self, elapsed)
    self.elapsed = self.elapsed + elapsed
    if self.elapsed < 1.0 then return end
    self.elapsed = 0

    local ah = AuctionHouseFrame or AuctionFrame
    if ah and ah:IsShown() then
        if not frame:IsShown() and not minimizeIcon:IsShown() then
            if isMinimized then ShowMinimized() else ShowMain() end
        end
    else
        HideAll()
    end
end)

-- ==========================================
-- 斜杠命令
-- ==========================================
SLASH_QUICKAUCTION1 = "/qa"
SlashCmdList["QUICKAUCTION"] = function(msg)
    msg = (msg or ""):lower()
    if msg == "show" then
        ShowMain()
    elseif msg == "hide" then
        HideAll()
    else
        print("|cffffff00[购物助手]|r 命令列表：")
        print("  /qa show  - 显示窗口")
        print("  /qa hide  - 隐藏窗口")
    end
end
