-------------------------------------------------------------------------------
-- RealNibba's Raid Tools - BiS_Tab.lua  v1.0
-- BiS Tracker tab with Item Browser, About, and How To Use sub-tabs.
-- Design: dark bg, blue borders, gold/green accents.
-------------------------------------------------------------------------------

local VERSION = "1.0"

local AceGUI = LibStub("AceGUI-3.0")

local nnBoxIsLinkTarget    = false
local g_pendingItemID      = 0
local g_pendingDisplayName = ""
local g_pendingLink        = ""

-------------------------------------------------------------------------------
-- Colour palette — neutral Ace3 tan/grey theme
-------------------------------------------------------------------------------
local C = {
    bg         = { 0.10, 0.10, 0.10, 0.00 },
    header     = { 0.16, 0.14, 0.11, 1    },
    row_odd    = { 0.12, 0.12, 0.12, 0.90 },
    row_even   = { 0.16, 0.16, 0.16, 0.90 },
    row_sel    = { 0.35, 0.28, 0.10, 1    },
    border     = { 0.55, 0.50, 0.42, 0.8  },
    title      = { 1.00, 0.82, 0.00, 1    },
    accent     = { 1.00, 0.82, 0.00, 1    },
    text       = { 0.90, 0.90, 0.90, 1    },
    dim        = { 0.60, 0.60, 0.60, 1    },
    red        = { 1.00, 0.30, 0.30, 1    },
    btn_normal = { 0.16, 0.14, 0.11, 1    },
    btn_hover  = { 0.28, 0.24, 0.18, 1    },
    btn_press  = { 0.08, 0.08, 0.08, 1    },
    gold       = { 1.00, 0.82, 0.00, 1    },
    green      = { 0.20, 1.00, 0.40, 1    },
}

-------------------------------------------------------------------------------
-- Helpers
-------------------------------------------------------------------------------
local function SetBG(f, r, g, b, a)
    if not f._bgTex then
        f._bgTex = f:CreateTexture(nil, "BACKGROUND")
        f._bgTex:SetAllPoints(f)
    end
    f._bgTex:SetTexture(r, g, b, a)
end

local function AddBorder(f, r, g, b, a, sz)
    sz = sz or 1
    local sides = {
        {"TOPLEFT","TOPRIGHT","SetHeight",sz},
        {"BOTTOMLEFT","BOTTOMRIGHT","SetHeight",sz},
        {"TOPLEFT","BOTTOMLEFT","SetWidth",sz},
        {"TOPRIGHT","BOTTOMRIGHT","SetWidth",sz},
    }
    for _, v in ipairs(sides) do
        local t = f:CreateTexture(nil, "OVERLAY")
        t:SetPoint(v[1]); t:SetPoint(v[2])
        t[v[3]](t, v[4])
        t:SetTexture(r, g, b, a)
    end
end

local function MakeButton(parent, label, w, h)
    local btn = CreateFrame("Button", nil, parent)
    btn:SetWidth(w or 90); btn:SetHeight(h or 22)
    SetBG(btn, unpack(C.btn_normal))
    AddBorder(btn, unpack(C.border))
    local fs = btn:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    fs:SetAllPoints(btn); fs:SetText(label)
    fs:SetTextColor(unpack(C.title))
    btn:SetScript("OnEnter",     function() SetBG(btn, unpack(C.btn_hover))  end)
    btn:SetScript("OnLeave",     function() SetBG(btn, unpack(C.btn_normal)) end)
    btn:SetScript("OnMouseDown", function() SetBG(btn, unpack(C.btn_press))  end)
    btn:SetScript("OnMouseUp",   function() SetBG(btn, unpack(C.btn_hover))  end)
    btn._label = fs
    return btn
end

local function MakeEditBox(parent, h)
    local eb = CreateFrame("EditBox", nil, parent)
    eb:SetHeight(h or 20)
    eb:SetFontObject("GameFontHighlightSmall")
    eb:SetTextColor(unpack(C.text))
    eb:SetAutoFocus(false); eb:EnableMouse(true)
    eb:SetMaxLetters(1024); eb:SetTextInsets(4, 4, 2, 2)
    SetBG(eb, 0.08, 0.08, 0.08, 0.97)
    AddBorder(eb, unpack(C.border))
    return eb
end

local function After(seconds, callback)
    local elapsed = 0
    local ticker  = CreateFrame("Frame", nil, UIParent)
    ticker:SetScript("OnUpdate", function()
        elapsed = elapsed + arg1
        if elapsed >= seconds then
            ticker:SetScript("OnUpdate", nil)
            ticker:Hide()
            callback()
        end
    end)
    ticker:Show()
end

-------------------------------------------------------------------------------
-- Hyperlink hook
-------------------------------------------------------------------------------
local _bisHookInstalled  = false
local _bisOrigInsertLink = nil
local g_nnBox            = nil

local function InstallBisLinkHook()
    if _bisHookInstalled then return end
    _bisHookInstalled  = true
    _bisOrigInsertLink = ChatEdit_InsertLink
    ChatEdit_InsertLink = function(link)
        if g_nnBox and nnBoxIsLinkTarget and link and link ~= "" then
            local itemID   = tonumber(link:match("|Hitem:(%d+):"))
            local itemName = link:match("%[(.-)%]")
            if itemName and itemName ~= "" then
                g_pendingItemID      = itemID or 0
                g_pendingDisplayName = itemName
                g_pendingLink        = link  -- store full hyperlink
                -- Show full hyperlink in the name box so user sees the actual link
                g_nnBox:SetText(link)
                g_nnBox:SetFocus()
                return true
            end
        end
        if _bisOrigInsertLink then return _bisOrigInsertLink(link) end
        return false
    end
end
InstallBisLinkHook()

-------------------------------------------------------------------------------
-- Icon loading
-------------------------------------------------------------------------------
local iconCache   = {}
local iconPending = {}
local iconQueried = {}

local queryTooltip = CreateFrame("GameTooltip", "RNTools_BisQueryTooltip", UIParent, "GameTooltipTemplate")
queryTooltip:SetOwner(UIParent, "ANCHOR_NONE")

local function RequestIcon(itemName, itemID)
    if iconQueried[itemName] then return end
    iconQueried[itemName] = true
    if itemID and itemID > 0 then
        queryTooltip:SetHyperlink("item:"..itemID..":0:0:0:0:0:0:0")
        queryTooltip:Hide()
    end
end

local function GetItemIconPath(itemName)
    if iconCache[itemName] then return iconCache[itemName] end
    local data   = BiSRaidDB and BiSRaidDB.items and BiSRaidDB.items[itemName]
    local itemID = data and data.item_id
    local icon
    if itemID and itemID > 0 then _, _, _, _, _, _, _, _, _, icon = GetItemInfo(itemID) end
    if not icon then _, _, _, _, _, _, _, _, _, icon = GetItemInfo(itemName) end
    if icon then iconCache[itemName] = icon; iconPending[itemName] = nil; return icon end
    iconPending[itemName] = true
    RequestIcon(itemName, itemID)
    return nil
end

-- Returns the |cAARRGGBB colour code matching the item's real rarity
-- (e.g. green/blue/purple/orange), falling back to grey for items with
-- no known item_id and purple for everything else.
local function GetItemQualityColorCode(data)
    local itemID = data and data.item_id
    if itemID and itemID > 0 then
        local _, _, quality = GetItemInfo(itemID)
        if quality and ITEM_QUALITY_COLORS[quality] then
            return ITEM_QUALITY_COLORS[quality].hex
        end
    end
    if data and data.item_link and data.item_link ~= "" then
        local colorCode = data.item_link:match("^(|c%x%x%x%x%x%x%x%x)")
        if colorCode then return colorCode end
    end
    if not itemID or itemID == 0 then return "|cff9d9d9d" end
    return "|cffA335EE"
end

local iconEventFrame = CreateFrame("Frame", "RNTools_BisIconEventFrame", UIParent)
iconEventFrame:RegisterEvent("GET_ITEM_INFO_RECEIVED")
iconEventFrame:SetScript("OnEvent", function()
    if not arg2 then return end
    local itemID = arg1
    if BiSRaidDB and BiSRaidDB.items then
        for name, _ in pairs(iconPending) do
            local d = BiSRaidDB.items[name]
            if d and d.item_id == itemID then
                local itemName, _, _, _, _, _, _, _, _, icon = GetItemInfo(itemID)
                if icon then iconCache[name] = icon; iconPending[name] = nil end
                if itemName and itemName ~= "" and (not d.display or d.display == "") then
                    d.display = itemName
                end
                -- Avoid a full row rebuild for late-arriving icon data; just
                -- patch the icon on the already-built row widget if present.
                local f = bisTabFrame
                local row = f and f._rowWidgets and f._rowWidgets[name]
                if icon and row then
                    row:SetImage(icon)
                    row:SetImageSize(ICON_SIZE, ICON_SIZE)
                elseif not (f and f._rowsBuilt) and RNTools_BisRefreshRows then
                    RNTools_BisRefreshRows()
                end
            end
        end
    end
end)

-------------------------------------------------------------------------------
-- Parse spec text
-------------------------------------------------------------------------------

-- DB display prefix → class suffix (for expanding "DK (Unholy, Frost)" → "Unholy Death Knight")
local CLASS_PREFIX = {
    ["dk"]     = "Death Knight",
    ["warr"]   = "Warrior",
    ["pala"]   = "Paladin",
    ["rog"]    = "Rogue",
    ["dru"]    = "Druid",
    ["priest"] = "Priest",
    ["sham"]   = "Shaman",
    ["mage"]   = "Mage",
    ["lock"]   = "Warlock",
    ["hunt"]   = "Hunter",
}

-- Canonical full spec name → {class abbr, short spec label}  (drives display rebuild)
local SPEC_DISPLAY = {
    ["Fury Warrior"]            = {"Warr",   "Fury"},
    ["Arms Warrior"]            = {"Warr",   "Arms"},
    ["Protection Warrior"]      = {"Warr",   "Protection"},
    ["Retribution Paladin"]     = {"Pala",   "Retribution"},
    ["Holy Paladin"]            = {"Pala",   "Holy"},
    ["Protection Paladin"]      = {"Pala",   "Protection"},
    ["Combat Rogue"]            = {"Rog",    "Combat"},
    ["Assassination Rogue"]     = {"Rog",    "Assassination"},
    ["Subtlety Rogue"]          = {"Rog",    "Subtlety"},
    ["Balance Druid"]           = {"Dru",    "Balance"},
    ["Feral Cat Druid"]         = {"Dru",    "Feral Cat"},
    ["Bear Tank Druid"]         = {"Dru",    "Bear Tank"},
    ["Restoration Druid"]       = {"Dru",    "Restoration"},
    ["Holy Priest"]             = {"Priest", "Holy"},
    ["Discipline Priest"]       = {"Priest", "Discipline"},
    ["Shadow Priest"]           = {"Priest", "Shadow"},
    ["Restoration Shaman"]      = {"Sham",   "Restoration"},
    ["Elemental Shaman"]        = {"Sham",   "Elemental"},
    ["Enhancement Shaman"]      = {"Sham",   "Enhancement"},
    ["Spellhancement Shaman"]   = {"Sham",   "Spellhancement"},
    ["Arcane Mage"]             = {"Mage",   "Arcane"},
    ["Fire Mage"]               = {"Mage",   "Fire"},
    ["Fire TTW Mage"]           = {"Mage",   "Fire TTW"},
    ["Frost Mage"]              = {"Mage",   "Frost"},
    ["Affliction Warlock"]      = {"Lock",   "Affliction"},
    ["Demonology Warlock"]      = {"Lock",   "Demonology"},
    ["Destruction Warlock"]     = {"Lock",   "Destruction"},
    ["Marksmanship Hunter"]     = {"Hunt",   "Marksmanship"},
    ["Survival Hunter"]         = {"Hunt",   "Survival"},
    ["Beast Mastery Hunter"]    = {"Hunt",   "Beast Mastery"},
    ["Unholy Death Knight"]     = {"DK",     "Unholy"},
    ["Frost Death Knight"]      = {"DK",     "Frost"},
    ["Blood Death Knight"]      = {"DK",     "Blood"},
    ["Blood Tank Death Knight"] = {"DK",     "Blood Tank"},
    ["Blood DK DPS"]            = {"DK",     "Blood DPS"},
}

-- User abbreviations/typos (lowercase) → canonical full spec name.
-- Must stay in sync with SPEC_ALIASES in MS_Changes.lua.
local SPEC_INPUT_NORM = {
    -- Warrior
    ["fury warrior"]            = "Fury Warrior",
    ["fury warr"]               = "Fury Warrior",
    ["fury"]                    = "Fury Warrior",
    ["arms warrior"]            = "Arms Warrior",
    ["arms warr"]               = "Arms Warrior",
    ["arms"]                    = "Arms Warrior",
    ["protection warrior"]      = "Protection Warrior",
    ["prot warrior"]            = "Protection Warrior",
    ["prot warr"]               = "Protection Warrior",
    ["prot war"]                = "Protection Warrior",
    -- Paladin
    ["retribution paladin"]     = "Retribution Paladin",
    ["retribution pala"]        = "Retribution Paladin",
    ["ret paladin"]             = "Retribution Paladin",
    ["ret pala"]                = "Retribution Paladin",
    ["retri paladin"]           = "Retribution Paladin",
    ["retri pala"]              = "Retribution Paladin",
    ["retri"]                   = "Retribution Paladin",
    ["ret"]                     = "Retribution Paladin",
    ["retribution"]             = "Retribution Paladin",
    ["holy paladin"]            = "Holy Paladin",
    ["holy pala"]               = "Holy Paladin",
    ["protection paladin"]      = "Protection Paladin",
    ["protection pala"]         = "Protection Paladin",
    ["prot paladin"]            = "Protection Paladin",
    ["prot pala"]               = "Protection Paladin",
    -- Rogue
    ["combat rogue"]            = "Combat Rogue",
    ["combat"]                  = "Combat Rogue",
    ["assassination rogue"]     = "Assassination Rogue",
    ["assassination"]           = "Assassination Rogue",
    ["subtlety rogue"]          = "Subtlety Rogue",
    ["subtlety"]                = "Subtlety Rogue",
    -- Druid
    ["balance druid"]           = "Balance Druid",
    ["balance"]                 = "Balance Druid",
    ["boomkin"]                 = "Balance Druid",
    ["boomie"]                  = "Balance Druid",
    ["moonkin"]                 = "Balance Druid",
    ["feral cat druid"]         = "Feral Cat Druid",
    ["feral cat"]               = "Feral Cat Druid",
    ["cat druid"]               = "Feral Cat Druid",
    ["bear tank druid"]         = "Bear Tank Druid",
    ["bear tank"]               = "Bear Tank Druid",
    ["bear druid"]              = "Bear Tank Druid",
    ["bear"]                    = "Bear Tank Druid",
    ["restoration druid"]       = "Restoration Druid",
    ["resto druid"]             = "Restoration Druid",
    ["tree"]                    = "Restoration Druid",
    -- Priest
    ["holy priest"]             = "Holy Priest",
    ["discipline priest"]       = "Discipline Priest",
    ["disc priest"]             = "Discipline Priest",
    ["disc"]                    = "Discipline Priest",
    ["discipline"]              = "Discipline Priest",
    ["shadow priest"]           = "Shadow Priest",
    ["shadow"]                  = "Shadow Priest",
    ["spriest"]                 = "Shadow Priest",
    ["sp"]                      = "Shadow Priest",
    -- Shaman
    ["restoration shaman"]      = "Restoration Shaman",
    ["resto shaman"]            = "Restoration Shaman",
    ["resto sham"]              = "Restoration Shaman",
    ["elemental shaman"]        = "Elemental Shaman",
    ["ele shaman"]              = "Elemental Shaman",
    ["ele sham"]                = "Elemental Shaman",
    ["elemental"]               = "Elemental Shaman",
    ["enhancement shaman"]      = "Enhancement Shaman",
    ["enh shaman"]              = "Enhancement Shaman",
    ["enh sham"]                = "Enhancement Shaman",
    ["enhancement"]             = "Enhancement Shaman",
    ["spellhancement shaman"]   = "Spellhancement Shaman",
    ["spellhancement"]          = "Spellhancement Shaman",
    -- Mage
    ["arcane mage"]             = "Arcane Mage",
    ["arcane"]                  = "Arcane Mage",
    ["fire mage"]               = "Fire Mage",
    ["fire"]                    = "Fire Mage",
    ["fire ttw mage"]           = "Fire TTW Mage",
    ["fire ttw"]                = "Fire TTW Mage",
    ["frost mage"]              = "Frost Mage",
    -- Warlock
    ["affliction warlock"]      = "Affliction Warlock",
    ["affliction"]              = "Affliction Warlock",
    ["demonology warlock"]      = "Demonology Warlock",
    ["demo warlock"]            = "Demonology Warlock",
    ["demonology"]              = "Demonology Warlock",
    ["demo"]                    = "Demonology Warlock",
    ["destruction warlock"]     = "Destruction Warlock",
    ["destro warlock"]          = "Destruction Warlock",
    ["destruction"]             = "Destruction Warlock",
    ["destro"]                  = "Destruction Warlock",
    -- Hunter
    ["marksmanship hunter"]     = "Marksmanship Hunter",
    ["mm hunter"]               = "Marksmanship Hunter",
    ["mm"]                      = "Marksmanship Hunter",
    ["marksmanship"]            = "Marksmanship Hunter",
    ["survival hunter"]         = "Survival Hunter",
    ["surv hunter"]             = "Survival Hunter",
    ["surv"]                    = "Survival Hunter",
    ["survival"]                = "Survival Hunter",
    ["beast mastery hunter"]    = "Beast Mastery Hunter",
    ["bm hunter"]               = "Beast Mastery Hunter",
    ["bm"]                      = "Beast Mastery Hunter",
    ["beast mastery"]           = "Beast Mastery Hunter",
    -- Death Knight
    ["unholy death knight"]     = "Unholy Death Knight",
    ["unholy dk"]               = "Unholy Death Knight",
    ["unholy"]                  = "Unholy Death Knight",
    ["frost death knight"]      = "Frost Death Knight",
    ["frost dk"]                = "Frost Death Knight",
    ["blood death knight"]      = "Blood Death Knight",
    ["blood dk"]                = "Blood Death Knight",
    ["blood"]                   = "Blood Death Knight",
    ["blood dk dps"]            = "Blood DK DPS",
    ["blood dps"]               = "Blood DK DPS",
    ["blood tank death knight"] = "Blood Tank Death Knight",
    ["blood tank dk"]           = "Blood Tank Death Knight",
    ["blood tank"]              = "Blood Tank Death Knight",
}

local function NormalizeSpecToken(s)
    return SPEC_INPUT_NORM[s:lower()] or s
end

local function ParseSpecText(raw)
    raw = raw:match("^%s*(.-)%s*$") or ""
    local fullNames = {}

    -- Split on "/" — handles both user input and existing DB format "CLASS (spec1, spec2)"
    for group in (raw .. "/"):gmatch("([^/]+)/") do
        group = group:match("^%s*(.-)%s*$")
        if group ~= "" then
            -- Check for DB grouped format: "DK (Unholy, Frost)"
            local classToken, specList = group:match("^(%a+)%s*%((.-)%)%s*$")
            if classToken and specList and CLASS_PREFIX[classToken:lower()] then
                local classSuffix = CLASS_PREFIX[classToken:lower()]
                for spec in (specList .. ","):gmatch("([^,]+),") do
                    spec = spec:match("^%s*(.-)%s*$")
                    if spec ~= "" then
                        local fullName = spec .. " " .. classSuffix
                        -- Prefer a known canonical name; fall back to constructed name
                        fullNames[#fullNames+1] = SPEC_DISPLAY[fullName] and fullName
                            or SPEC_INPUT_NORM[(spec .. " " .. classSuffix):lower()]
                            or fullName
                    end
                end
            else
                -- Individual abbreviation or full name
                fullNames[#fullNames+1] = NormalizeSpecToken(group)
            end
        end
    end

    -- Rebuild display string grouped by class: "Pala (Retribution) / Sham (Restoration)"
    local classOrder  = {}
    local classOrderSet = {}
    local classSpecs  = {}
    for _, fullName in ipairs(fullNames) do
        local info = SPEC_DISPLAY[fullName]
        if info then
            local cls = info[1]
            if not classOrderSet[cls] then
                classOrderSet[cls] = true
                classOrder[#classOrder+1] = cls
                classSpecs[cls] = {}
            end
            classSpecs[cls][#classSpecs[cls]+1] = info[2]
        end
    end

    local parts   = {}
    local usedCls = {}
    for _, fullName in ipairs(fullNames) do
        local info = SPEC_DISPLAY[fullName]
        if info then
            local cls = info[1]
            if not usedCls[cls] then
                usedCls[cls] = true
                parts[#parts+1] = cls .. " (" .. table.concat(classSpecs[cls], ", ") .. ")"
            end
        else
            parts[#parts+1] = fullName  -- unrecognised: keep as-is
        end
    end

    local normRaw = table.concat(parts, " / ")
    return fullNames, normRaw
end

-------------------------------------------------------------------------------
-- State
-------------------------------------------------------------------------------
local selectedItem = nil
local searchFilter = ""
local sortedKeys   = {}
local ICON_SIZE    = 16
local bisTabFrame  = nil

local function RebuildKeys()
    sortedKeys = {}
    if not BiSRaidDB or not BiSRaidDB.items then return end
    for k in pairs(BiSRaidDB.items) do
        if searchFilter == "" or k:lower():sub(1, #searchFilter) == searchFilter then
            sortedKeys[table.getn(sortedKeys)+1] = k
        end
    end
    table.sort(sortedKeys)
end

-- Updates only the gold-text/highlight-bar state of a single row widget,
-- without releasing/recreating any AceGUI widgets (avoids a full list
-- rebuild + scroll-reset + icon flicker every time selection changes).
local function RNTools_BisSetRowSelected(key, selected)
    local f = bisTabFrame
    local lbl = f and f._rowWidgets and f._rowWidgets[key]
    if not lbl then return end
    if selected then
        lbl:SetColor(unpack(C.gold))
        if lbl._selBg then lbl._selBg:Show() end
    else
        lbl:SetColor(unpack(C.text))
        if lbl._selBg then lbl._selBg:Hide() end
    end
end

function RNTools_BisRefreshRows()
    local f = bisTabFrame; if not f or not f._itemScroll then return end
    local scroll = f._itemScroll
    scroll:ReleaseChildren()
    f._rowWidgets = {}
    for idx, key in ipairs(sortedKeys) do
        local data = BiSRaidDB.items[key]
        local lbl  = AceGUI:Create("InteractiveLabel")
        lbl:SetFullWidth(true)
        local icon = GetItemIconPath(key)
        lbl:SetImage(icon or "Interface\\Icons\\INV_Misc_QuestionMark")
        lbl:SetImageSize(ICON_SIZE, ICON_SIZE)
        local displayName = (data and data.display and data.display ~= "") and data.display or key
        local itemColor = GetItemQualityColorCode(data)
        lbl:SetText("|cff446688"..idx.."|r  "..itemColor.."["..displayName.."]|r")
        -- Persistent selection highlight (background bar behind the row)
        if not lbl._selBg then
            local bg = lbl.frame:CreateTexture(nil, "BACKGROUND")
            bg:SetAllPoints(lbl.frame)
            lbl._selBg = bg
        end
        if key == selectedItem then
            lbl:SetColor(unpack(C.gold))
            lbl._selBg:SetTexture(unpack(C.row_sel))
            lbl._selBg:Show()
        else
            lbl:SetColor(unpack(C.text))
            lbl._selBg:Hide()
        end
        f._rowWidgets[key] = lbl
        lbl:SetCallback("OnClick", function(widget, event, button)
            if IsShiftKeyDown() then
                f._newName:SetText(key); f._newName.editbox:SetFocus()
            else
                local prev = selectedItem
                selectedItem = key
                RNTools_BisLoadEditor(selectedItem)
                if prev and prev ~= key then RNTools_BisSetRowSelected(prev, false) end
                RNTools_BisSetRowSelected(key, true)
            end
        end)
        lbl:SetCallback("OnEnter", function(widget)
            local data2   = BiSRaidDB and BiSRaidDB.items and BiSRaidDB.items[key]
            local itemID  = data2 and data2.item_id
            GameTooltip:SetOwner(widget.frame, "ANCHOR_RIGHT")
            local got = false
            if itemID and itemID > 0 then
                GameTooltip:SetHyperlink("item:"..itemID..":0:0:0:0:0:0:0"); got = true
            else
                local _, link = GetItemInfo(key)
                if link then GameTooltip:SetHyperlink(link); got = true end
            end
            if not got then
                GameTooltip:AddLine("|cffFFD100"..key.."|r")
                GameTooltip:AddLine("Hover again once icon loads.", 0.7,0.7,0.7)
            end
            if data2 then
                local bisD = data2.bis_text and data2.bis_text ~= "" and data2.bis_text
                    or (data2.bis and table.getn(data2.bis) > 0 and table.concat(data2.bis,", "))
                local altD = data2.alt_text and data2.alt_text ~= "" and data2.alt_text
                    or (data2.alt and table.getn(data2.alt) > 0 and table.concat(data2.alt,", "))
                if bisD then GameTooltip:AddLine(" "); GameTooltip:AddLine("|cff20ff60BiS:|r "..bisD, 1,1,1,true) end
                if altD then GameTooltip:AddLine("|cffFFD100Alt:|r "..altD, 1,1,1,true) end
            end
            GameTooltip:Show()
        end)
        lbl:SetCallback("OnLeave", function() GameTooltip:Hide() end)
        scroll:AddChild(lbl)
    end
    f._rowsBuilt = true
end

function RNTools_BisRefreshUI()
    if not bisTabFrame then return end
    RebuildKeys()
    local dbCount = 0
    if BiSRaidDB and BiSRaidDB.items then
        for _ in pairs(BiSRaidDB.items) do dbCount = dbCount + 1 end
    end
    if bisTabFrame._totFS  then bisTabFrame._totFS:SetText(dbCount.." items in database") end
    if bisTabFrame._countFS then bisTabFrame._countFS:SetText(table.getn(sortedKeys).." shown") end
    RNTools_BisRefreshRows()
end

-- Returns the item's name wrapped in its real hyperlink colour (matching the
-- colour the item link would show in chat), falling back to the same
-- quality-based colouring used in the item list rows.
local function GetItemColoredText(key, data)
    local displayName = (data and data.display and data.display ~= "") and data.display or key
    if data and data.item_link and data.item_link ~= "" then
        return data.item_link
    end
    local itemID = data and data.item_id
    if itemID and itemID > 0 then
        local _, link = GetItemInfo(itemID)
        if link then return link end
    end
    local itemColor = (not itemID or itemID == 0) and "|cff9d9d9d" or "|cffA335EE"
    return itemColor.."["..displayName.."]|r"
end

function RNTools_BisLoadEditor(key)
    local f = bisTabFrame; if not f then return end
    local data = key and BiSRaidDB and BiSRaidDB.items and BiSRaidDB.items[key]
    if not data then
        f._edHdr:SetText("Select an item to edit")
        f._edItemName:SetText(""); f._bisBox:SetText(""); f._altBox:SetText("")
        f._statusFS:SetText(""); return
    end
    f._edHdr:SetText("Editing:")
    f._edItemName:SetText(GetItemColoredText(key, data))
    -- Build BiS display text
    local bisDisplay = ""
    if type(data.bis_text) == "string" and data.bis_text ~= "" and data.bis_text ~= "(None)" then
        bisDisplay = data.bis_text
    elseif type(data.bis) == "table" then
        local parts = {}; local n = 0
        for i = 1, 200 do
            if data.bis[i] then n = n + 1; parts[n] = data.bis[i] else break end
        end
        if n > 0 then bisDisplay = table.concat(parts, " / ") end
    end
    -- Build Alt display text
    local altDisplay = ""
    if type(data.alt_text) == "string" and data.alt_text ~= "" and data.alt_text ~= "(None)" then
        altDisplay = data.alt_text
    elseif type(data.alt) == "table" then
        local parts = {}; local n = 0
        for i = 1, 200 do
            if data.alt[i] then n = n + 1; parts[n] = data.alt[i] else break end
        end
        if n > 0 then altDisplay = table.concat(parts, " / ") end
    elseif type(data.alt) == "string" and data.alt ~= "" then
        altDisplay = data.alt
    end
    -- Show text if available, otherwise grey placeholder
    if bisDisplay ~= "" then
        f._bisBox:SetText(bisDisplay)
        f._bisBox.editbox:SetTextColor(0.90, 0.90, 0.90, 1)
    else
        f._bisBox:SetText("(None)")
        f._bisBox.editbox:SetTextColor(0.4, 0.4, 0.4, 1)
    end
    if altDisplay ~= "" then
        f._altBox:SetText(altDisplay)
        f._altBox.editbox:SetTextColor(0.90, 0.90, 0.90, 1)
    else
        f._altBox:SetText("(None)")
        f._altBox.editbox:SetTextColor(0.4, 0.4, 0.4, 1)
    end
    f._statusFS:SetText("")
    f._delBtn._confirm = nil
    f._delBtn:SetText("|cffff4d4dDelete Item|r")
end

-------------------------------------------------------------------------------
-- Build About panel
-------------------------------------------------------------------------------
local function BuildAboutPanel(parent)
    local p = CreateFrame("Frame", nil, parent)
    p:SetAllPoints(parent)
    p:Hide()

    local title = p:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    title:SetPoint("TOP", 0, -20)
    title:SetTextColor(unpack(C.gold))
    title:SetText("RealNibba's Raid Tools  v"..VERSION)

    local sub = p:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    sub:SetPoint("TOP", title, "BOTTOM", 0, -6)
    sub:SetTextColor(unpack(C.green))
    sub:SetText("Made with Love for RealNibba's Warmane Community")

    -- divider
    local div = p:CreateTexture(nil, "ARTWORK")
    div:SetHeight(1); div:SetTexture(unpack(C.border))
    div:SetPoint("TOPLEFT", 40, -58); div:SetPoint("TOPRIGHT", -40, -58)

    local msg = p:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    msg:SetPoint("TOP", 0, -74)
    msg:SetPoint("LEFT", 40, 0); msg:SetPoint("RIGHT", -40, 0)
    msg:SetJustifyH("CENTER"); msg:SetTextColor(0.90, 0.90, 0.90, 1)
    msg:SetText(
        "You are the reason this addon exists.\n"..
        "Every viewer, every subscriber, every kind word\n"..
        "pushes me to keep creating and improving.\n\n"..
        "Keep grinding, keep improving, keep being awesome.\n"..
        "Together we make Warmane a better place!"
    )

    -- YouTube
    local ytLabel = p:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    ytLabel:SetPoint("TOP", msg, "BOTTOM", 0, -20)
    ytLabel:SetTextColor(1, 0.2, 0.2, 1); ytLabel:SetText("YouTube Channel")

    local ytLink = p:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    ytLink:SetPoint("TOP", ytLabel, "BOTTOM", 0, -4)
    ytLink:SetTextColor(0.4, 0.7, 1.0, 1); ytLink:SetText("youtube.com/@RealNibbaWarmane")

    local ytBtn = AceGUI:Create("Button")
    ytBtn:SetText("Copy YouTube URL")
    ytBtn:SetWidth(200)
    ytBtn.frame:SetParent(p)
    ytBtn.frame:ClearAllPoints()
    ytBtn.frame:SetPoint("TOP", ytLink, "BOTTOM", 0, -6)
    ytBtn.frame:Show()
    ytBtn:SetCallback("OnClick", function()
        DEFAULT_CHAT_FRAME:AddMessage("|cffFFD100RealNibba YouTube:|r https://www.youtube.com/@RealNibbaWarmane")
        ytBtn:SetText("Printed to chat!")
        After(3, function() ytBtn:SetText("Copy YouTube URL") end)
    end)

    -- Discord
    local dcLabel = p:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    dcLabel:SetPoint("TOP", ytBtn.frame, "BOTTOM", 0, -18)
    dcLabel:SetTextColor(0.4, 0.6, 1.0, 1); dcLabel:SetText("Discord Community")

    local dcLink = p:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    dcLink:SetPoint("TOP", dcLabel, "BOTTOM", 0, -4)
    dcLink:SetTextColor(0.4, 0.7, 1.0, 1); dcLink:SetText("discord.com/invite/uZTTqejway")

    local dcBtn = AceGUI:Create("Button")
    dcBtn:SetText("Copy Discord URL")
    dcBtn:SetWidth(200)
    dcBtn.frame:SetParent(p)
    dcBtn.frame:ClearAllPoints()
    dcBtn.frame:SetPoint("TOP", dcLink, "BOTTOM", 0, -6)
    dcBtn.frame:Show()
    dcBtn:SetCallback("OnClick", function()
        DEFAULT_CHAT_FRAME:AddMessage("|cffFFD100RealNibba Discord:|r https://discord.com/invite/uZTTqejway")
        dcBtn:SetText("Printed to chat!")
        After(3, function() dcBtn:SetText("Copy Discord URL") end)
    end)

    local footer = p:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    footer:SetPoint("BOTTOM", 0, 10)
    footer:SetJustifyH("CENTER"); footer:SetTextColor(unpack(C.dim))
    footer:SetText("RealNibba's Raid Tools v"..VERSION.." — Thank you for your support!")

    return p
end

-------------------------------------------------------------------------------
-- Build How To Use panel  (BiS Tracker specific -- detailed guide)
-------------------------------------------------------------------------------
local function BuildHowToPanel(parent)
    local p = CreateFrame("Frame", nil, parent)
    p:SetAllPoints(parent)
    p:Hide()

    local title = p:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    title:SetPoint("TOPLEFT", 10, -14); title:SetPoint("TOPRIGHT", -10, -14)
    title:SetJustifyH("CENTER")
    title:SetTextColor(unpack(C.gold))
    title:SetText("BiS Item Tracker -- How To Use")

    local div = p:CreateTexture(nil, "ARTWORK")
    div:SetHeight(1); div:SetTexture(unpack(C.border))
    div:SetPoint("TOPLEFT", 10, -36); div:SetPoint("TOPRIGHT", -10, -36)

    local sf = CreateFrame("ScrollFrame", "RNTools_BisHowToScroll", p, "UIPanelScrollFrameTemplate")
    sf:SetPoint("TOPLEFT", 10, -42); sf:SetPoint("BOTTOMRIGHT", -28, 10)

    local content = CreateFrame("Frame", nil, sf)
    content:SetWidth(600)
    content:SetHeight(2000)  -- pre-set so FontStrings render before height is measured
    sf:SetScrollChild(content)
    p:SetScript("OnShow", function()
        local w = sf:GetWidth()
        if w and w > 10 then content:SetWidth(w) end
    end)

    local sections = {
        { h=true,  t="What Does the BiS Tracker Do?" },
        { h=false, t="When an officer types /rw [Item Name] in raid chat, the BiS Tracker reads the item link, looks it up in the built-in database, and automatically posts the BiS and Alternative specs to /raid chat." },
        { h=false, t="Example: officer types /rw [Shadowmourne] and every raider sees:" },
        { h=false, t="[Shadowmourne]  BiS: Fury Warrior / Unholy DK / Blood DK DPS / Ret Paladin / Combat Rogue" },
        { s=true },
        { h=true,  t="Enabling and Disabling" },
        { h=false, t="Click the ON / OFF button at the top right of the BiS Tracker tab to toggle announcements for your character. When OFF, your client will never post to raid chat even if others do." },
        { h=false, t="Your ON/OFF preference is saved between sessions." },
        { s=true },
        { h=true,  t="Spam Protection (Multiple Users)" },
        { h=false, t="If multiple raid members have RNTools installed, only ONE will post per item." },
        { h=false, t="When /rw [Item] is detected, every client immediately sends a hidden CLAIM message" },
        { h=false, t="to all other addon users in the raid, then waits 2 seconds before posting." },
        { h=false, t="Every client that receives a CLAIM cancels its own pending announcement." },
        { h=false, t="The fastest client (first to send CLAIM) is the one that posts to raid chat." },
        { h=false, t="A 15-second per-item cooldown prevents re-announcing if /rw is repeated." },
        { s=true },
        { h=true,  t="Adding a New Item -- Shift+Click Method (Recommended)" },
        { h=false, t="1. Click the Item Name box in the Add New Item section to focus it." },
        { h=false, t="2. Shift+Click any item from your bags, character sheet, or a link in chat." },
        { h=false, t="3. The item name fills in automatically. The item ID is saved for icon and tooltip loading." },
        { h=false, t="4. Type BiS Specs:  Fury Warrior / Ret Paladin  (commas and slashes both work as separators)." },
        { h=false, t="5. Optionally fill Alt Specs for alternative / allowed-to-roll classes." },
        { h=false, t="6. Click Add Item. The item appears in the list immediately with its icon." },
        { s=true },
        { h=true,  t="Adding a New Item -- Manual Method" },
        { h=false, t="Type the item name in lowercase in the Item Name box (e.g.  shadowmourne )." },
        { h=false, t="Fill in BiS and/or Alt specs. Click Add Item. The icon may show as ? if the item ID is unknown — hover the row to trigger a server fetch." },
        { s=true },
        { h=true,  t="Editing an Existing Item" },
        { h=false, t="Click any row in the item list. The editor on the right loads the item's current BiS and Alt specs." },
        { h=false, t="If a field is empty it shows (None) as a placeholder — clear this before typing new specs." },
        { h=false, t="Modify the specs and click Save Changes. The change takes effect immediately." },
        { h=false, t="Shift+Click a row to pre-fill the Add New name box instead of loading the editor." },
        { s=true },
        { h=true,  t="Deleting an Item" },
        { h=false, t="Select the item from the list, click Delete Item, then click a second time to confirm. The two-step prevents accidental deletions." },
        { s=true },
        { h=true,  t="Restoring the Default Database" },
        { h=false, t="Click Restore Defaults at the bottom right of the editor, then confirm. This wipes ALL custom changes and restores the full ICC and Ruby Sanctum BiS database." },
        { s=true },
        { h=true,  t="Faction Support (Alliance vs Horde)" },
        { h=false, t="The addon detects your faction at login and loads the correct item names." },
        { h=false, t="Alliance characters see Death's Verdict; Horde characters see Death's Choice. Garrosh's Rage, Sylvanas' Cunning, Belt of the Merciless/Pitiless Killer, and Juggernaut's Vitality are handled the same way." },
        { h=false, t="Type /rnt reload when logging onto an alt of a different faction to refresh the database." },
        { s=true },
        { h=true,  t="Icons and Tooltips" },
        { h=false, t="Icons load from the game server. Items you have never seen in-game may show a ? on first login — hover the row to request the data." },
        { h=false, t="Item names in the list are coloured purple (epic/heroic quality). Items without an ID are shown in grey." },
        { h=false, t="Hovering any row shows the full item tooltip plus BiS and Alt spec information." },
        { s=true },
        { h=true,  t="Searching" },
        { h=false, t="Use the Search box above the item list to filter by name. Results update as you type. Press Escape to clear the search." },
        { s=true },
        { h=true,  t="Slash Commands" },
        { h=false, t="/rnt              -- open or close the addon window" },
        { h=false, t="/rnt reload       -- reload and re-detect faction for the BiS database" },
    }

    local totalH = 8
    for _, entry in ipairs(sections) do
        if entry.s then
            totalH = totalH + 10
        elseif entry.h then
            local fs = content:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            fs:SetPoint("TOPLEFT",  content, "TOPLEFT",  8, -totalH)
            fs:SetPoint("TOPRIGHT", content, "TOPRIGHT", -8, -totalH)
            fs:SetJustifyH("LEFT")
            fs:SetTextColor(unpack(C.gold))
            fs:SetText(entry.t)
            totalH = totalH + 22
        else
            local fs = content:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
            fs:SetPoint("TOPLEFT",  content, "TOPLEFT",  18, -totalH)
            fs:SetPoint("TOPRIGHT", content, "TOPRIGHT", -8, -totalH)
            fs:SetJustifyH("LEFT")
            fs:SetNonSpaceWrap(true)
            fs:SetTextColor(unpack(C.text))
            fs:SetText(entry.t)
            local fh = fs:GetHeight()
            totalH = totalH + (fh and fh > 18 and fh + 2 or 18)
        end
    end
    content:SetHeight(totalH + 20)

    return p
end



-------------------------------------------------------------------------------
-- FAQ panel -- lives in BiS_Tab.lua so it has access to C, SetBG, etc.
-- Called from UI_Core as the FAQ tab. Identical structure to BuildHowToPanel.
-------------------------------------------------------------------------------
function CreateFAQTabContent(parent, onClose)
    local f = CreateFrame("Frame", nil, parent)
    f:SetAllPoints(parent)
    f:Hide()
    SetBG(f, unpack(C.bg))

    local title = f:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    title:SetPoint("TOPLEFT", f, "TOPLEFT", 10, -14)
    title:SetPoint("TOPRIGHT", f, "TOPRIGHT", -10, -14)
    title:SetJustifyH("CENTER")
    title:SetTextColor(unpack(C.gold))
    title:SetText("FAQ  --  How to Use RealNibba's Raid Tools")

    local div = f:CreateTexture(nil, "ARTWORK")
    div:SetHeight(1)
    div:SetTexture(unpack(C.border))
    div:SetPoint("TOPLEFT",  f, "TOPLEFT",  10, -36)
    div:SetPoint("TOPRIGHT", f, "TOPRIGHT", -10, -36)

    local sf = CreateFrame("ScrollFrame", "RNTools_FAQScroll", f, "UIPanelScrollFrameTemplate")
    sf:SetPoint("TOPLEFT",     f, "TOPLEFT",     10, -42)
    sf:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -28,  8)

    local content = CreateFrame("Frame", nil, sf)
    content:SetWidth(820)
    content:SetHeight(2000)
    sf:SetScrollChild(content)

    f:SetScript("OnShow", function()
        local w = sf:GetWidth()
        if w and w > 10 then content:SetWidth(w) end
    end)

    local sections = {
        { h=true, t="GENERAL" },
        { q=true, t="How do I open the addon?" },
        { a=true, t="/rnt or /rntools in chat, or left-click the minimap icon." },
        { q=true, t="What is the status bar on my screen?" },
        { a=true, t="BT=BiS Tracker  SPAM=Spammer  AR=Auto Reply  ML=Master Looter  AI=AutoInvite  MS=MS+Buffs  RB=Raid Browser" },
        { a=true, t="Green pill = module enabled.  Red = disabled.  Click a pill to toggle that module." },
        { a=true, t="Left-drag to move the bar anywhere.  Right-click to open the main window." },
        { q=true, t="What is the Quick Access panel?" },
        { a=true, t="A compact floating panel with module toggles, roll shortcuts, spec DB status and announce buttons." },
        { a=true, t="Open it with: /rnt qa, or the QA button on the right of the status bar, or the minimap icon." },
        { q=true, t="How do I open Settings/Config?" },
        { a=true, t="Click the Config tab in the main window, or type /rnt config." },
        { a=true, t="Settings include: scale, Roll Manager defaults, Raid Browser channels, Spec DB, Status Bar pills, and more." },
        { q=true, t="How do I resize the window?" },
        { a=true, t="Use the scale slider in the Config tab (/rnt config). Range: 50%-200%." },
        { q=true, t="Does it work for Alliance and Horde?" },
        { a=true, t="Yes. Faction auto-detected at login. Alliance sees Death's Verdict, Horde sees Death's Choice." },
        { a=true, t="Type /rnt reload when switching to a different faction alt." },
        { s=true },
        { h=true, t="PROFILES (Config tab)" },
        { q=true, t="What are profiles?" },
        { a=true, t="Each profile stores a complete set of settings. Like ElvUI profiles — great for alts with different setups." },
        { q=true, t="How do I create a profile?" },
        { a=true, t="Config tab -> Profiles section -> New button -> type a name. Profile starts with default values." },
        { q=true, t="How do I copy settings to a new profile?" },
        { a=true, t="Config tab -> Profiles -> Copy button -> type the target profile name. Creates or overwrites it." },
        { q=true, t="How do I share or backup settings?" },
        { a=true, t="Config tab -> Export Profile -> copy the string. To restore: Config tab -> Import Profile -> paste." },
        { s=true },
        { h=true, t="BIS TRACKER" },
        { q=true, t="What does the BiS Tracker do?" },
        { a=true, t="Officer types /rw [Item Name] in raid. Addon looks it up and posts BiS specs to /raid chat." },
        { a=true, t="Example: /rw [Shadowmourne]  ->  [Shadowmourne] BiS: Fury Warrior / Unholy DK / ..." },
        { q=true, t="How do I turn it on or off?" },
        { a=true, t="ON/OFF button top-right of the BiS Tracker tab, or click the BT pill on the status bar." },
        { q=true, t="Multiple users — will it spam?" },
        { a=true, t="No. Each client rolls a random 0.8-1.6s delay and broadcasts a hidden CLAIM to the raid." },
        { a=true, t="The client with the lowest random number wins and is the only one that announces." },
        { a=true, t="15-second cooldown per item prevents re-announcing the same item." },
        { q=true, t="How do I add an item?" },
        { a=true, t="Item Browser -> Add New Item. Click the name box then Shift+Click any item to auto-fill." },
        { a=true, t="Fill BiS and Alt specs. Click Add Item." },
        { q=true, t="How do I edit an item?" },
        { a=true, t="Click any row in the list. Edit specs in the right panel. Click Save Changes." },
        { s=true },
        { h=true, t="MASTER LOOTER" },
        { q=true, t="What is the Loot Tracker?" },
        { a=true, t="Tracks all BoP and BoE drops in the raid and whispers a collector on every drop." },
        { a=true, t="Set BoP and BoE Collector names -> tick Whisper Collector -> Enable Loot Tracker." },
        { a=true, t="Collector receives: [RNTools] BoP looted: [Shadowmourne] by PlayerName." },
        { q=true, t="What does Set Myself as ML do?" },
        { a=true, t="One-click Master Looter assignment. Requires raid leader or assist rank." },
        { s=true },
        { h=true, t="ROLL MANAGER  (Mini Roll Manager: /rnt miniml)" },
        { q=true, t="What is the Roll Manager?" },
        { a=true, t="Runs structured item rolls for RL/ML. Shows who rolled, their spec, and whether they're eligible." },
        { a=true, t="Also works for non-RL: listens to raid chat and tracks rolls automatically as a spectator." },
        { a=true, t="Use /rnt miniml to open the compact floating Mini Roll Manager window." },
        { q=true, t="How does a roll work (RL/ML)?" },
        { a=true, t="1. Add an item to the item list (or drag one in)." },
        { a=true, t="2. Click the item -> it appears selected. Click Open Rolls." },
        { a=true, t="3. Addon announces the item in Raid Warning and tells eligible raiders to /roll." },
        { a=true, t="4. Rolls appear in the roll list, color-coded: green=eligible, red=wrong spec." },
        { a=true, t="5. A countdown timer fires. When done, announce the winner with one click." },
        { q=true, t="What is spec enforcement?" },
        { a=true, t="If a raider's recorded spec (from Spec DB) doesn't match the item's BiS list, their roll is flagged red." },
        { a=true, t="The RL still sees the roll and can award it manually. Enforcement is advisory, not blocking." },
        { a=true, t="Toggle spec enforcement globally in Config -> Roll Manager -> Spec Enforcement." },
        { q=true, t="What is [Done] / [Timeout] / [No Winner]?" },
        { a=true, t="[Done] = item was awarded. Click it to see the full roll history for that item." },
        { a=true, t="[Timeout] = 45-second countdown expired with no Rolls CLOSED message (non-RL only)." },
        { a=true, t="[No Winner] = RL closed rolls with no eligible candidates / item skipped." },
        { s=true },
        { h=true, t="SPAMMER" },
        { q=true, t="What does it do?" },
        { a=true, t="Sends a recurring message to General, Trade, Global, Yell, or custom channels on a timer." },
        { q=true, t="How do I start it?" },
        { a=true, t="Select or create a preset -> type your message -> set intervals per channel -> Start Spamming." },
        { a=true, t="The Start button turns green while active. Click Stop to end the loop." },
        { q=true, t="Why is Warmane muting me?" },
        { a=true, t="Keep intervals 20+ seconds. Safe defaults: General 31s  Trade 23s  Global/World 71s  Yell 41s." },
        { q=true, t="What is the Raid Builder (RP Builder)?" },
        { a=true, t="The right panel builds a formatted LFM message for your raid. Select raid type, difficulty, size." },
        { a=true, t="Choose roles and class requirements, optionally include an achievement link, then Send to Spammer." },
        { a=true, t="The ALL CLASSES toggle overrides all class/role selections with a generic 'all classes welcome' line." },
        { q=true, t="Can I have different messages per character?" },
        { a=true, t="Yes. Create a named preset per character. Rename, Delete, or Save presets from the preset row." },
        { s=true },
        { h=true, t="AUTO REPLY" },
        { q=true, t="What does it do?" },
        { a=true, t="Replies automatically to whispers matching your configured commands." },
        { a=true, t="Example: player whispers ?discord -> they receive your Discord link instantly." },
        { q=true, t="How do I set it up?" },
        { a=true, t="Auto Reply tab -> select or create a preset -> set Default Response -> add commands -> Enable." },
        { q=true, t="What commands work with zero setup?" },
        { a=true, t="?help or ?commands  ->  sends the full command list to the whisperer." },
        { a=true, t="?bis <item name>    ->  live BiS lookup. Example: ?bis shadowmourne" },
        { q=true, t="How do I reorder commands?" },
        { a=true, t="Drag the grip handle on the left of each command row up or down." },
        { q=true, t="What is the default response cooldown?" },
        { a=true, t="7 minutes per sender. Prevents flooding if someone spams the same whisper." },
        { s=true },
        { h=true, t="AUTO INVITE" },
        { q=true, t="What does it do?" },
        { a=true, t="Invites players who whisper a keyword. Example: player whispers + or inv -> instant invite." },
        { q=true, t="How do I set it up?" },
        { a=true, t="AutoInvite tab -> add keywords -> optionally enable Guild Chat listen -> set response -> Enable." },
        { q=true, t="Exact vs Contains?" },
        { a=true, t="Exact: the entire whisper must match the keyword exactly (case-insensitive)." },
        { a=true, t="Contains: keyword can appear anywhere in the message." },
        { q=true, t="White/Blacklist?" },
        { a=true, t="Add player name -> tick Whitelist: always invited, bypasses keyword check." },
        { a=true, t="Add player name -> untick Whitelist (Blacklist): always ignored even if they type the keyword." },
        { a=true, t="10-second cooldown per player prevents invite spam." },
        { s=true },
        { h=true, t="MS+BUFFS  —  MS CHANGES + BUFF ORDER + SPEC DATABASE" },
        { q=true, t="What does MS Changes do?" },
        { a=true, t="Tracks Main Spec changes from raid members received via whisper. Saved on logout." },
        { q=true, t="How do I collect spec changes?" },
        { a=true, t="1. Click DEMAND MS Changes -> sends a /rw telling raid to whisper you their spec." },
        { a=true, t="2. Players whisper e.g: Fury, Holy, Resto, Bear, Ret, Shadow, Boomie, Unholy..." },
        { a=true, t="3. Valid specs are auto-recorded. Invalid specs get a helpful reply listing valid options for their class." },
        { a=true, t="4. Click CLOSE when done -> sends a /rw that changes are closed." },
        { a=true, t="5. Click Announce All Changes -> posts all recorded changes to /rw." },
        { q=true, t="How does class-based spec validation work?" },
        { a=true, t="The addon reads the player's class from the raid roster and only accepts specs valid for that class." },
        { a=true, t="Example: a Hunter whispering 'tank' gets a reply listing Hunter's valid specs only." },
        { a=true, t="Generic roles also work: 'tank' -> resolves to the tank spec for that class (Prot/Blood Tank/Bear)." },
        { a=true, t="'healer' -> Holy/Disc/Resto.  'dps' -> the default DPS spec for that class." },
        { q=true, t="What abbreviations are accepted?" },
        { a=true, t="DK: bdk=Blood Tank, fdk/frost dk=Frost, udk=Unholy, blood dps=Blood DPS (exact)." },
        { a=true, t="Druid: cat/feral=Feral Cat, bear/bear tank=Feral Bear, boomie/moonkin=Balance, tree=Restoration." },
        { a=true, t="Paladin: ret/reti=Retribution, prot=Protection, holy=Holy." },
        { a=true, t="Shaman: enha/encha=Enhancement, ele=Elemental, resto=Restoration." },
        { a=true, t="Priest: disco/disc=Discipline, sp/shp=Shadow, holy=Holy." },
        { a=true, t="Rogue: sin/assa=Assassination, combat=Combat, sub=Subtlety." },
        { a=true, t="Warrior: arms=Arms, fury=Fury, prot=Protection." },
        { a=true, t="Mage: arcane=Arcane, fire=Fire, frost=Frost." },
        { a=true, t="Warlock: aff/affliction=Affliction, demo/demonology=Demonology, destro=Destruction." },
        { a=true, t="Hunter: bm=Beast Mastery, mm/marks=Marksmanship, surv/survival=Survival." },
        { q=true, t="How do I share MS Changes with other officers?" },
        { a=true, t="MS+Buffs tab -> Share MS + Buffs button -> sends MS Changes + Buff Order to all Assist/RL/ML in raid." },
        { a=true, t="Recipients must also have RNTools installed. Each officer gets a whisper confirmation." },
        { a=true, t="Tick 'Auto-share on close' to share automatically when you click CLOSE." },
        { q=true, t="What is Buff Order?" },
        { a=true, t="Assign which paladin gives which Greater Blessing (GBoM, GBoW, GBoS, GBoK)." },
        { a=true, t="Use Auto Assign to auto-distribute by inspect spec: Ret->GBoM, Holy->GBoW, Prot->GBoS, +GBoK." },
        { a=true, t="Auto Assign reads actual inspected specs, not MS Changes overrides." },
        { a=true, t="Click Announce Buff Order to post the assignments to raid chat." },
        { q=true, t="What is the Spec Database?" },
        { a=true, t="Stores each raider's talent spec (gathered via inspect). Used by the Roll Manager for eligibility." },
        { q=true, t="How do I gather specs?" },
        { a=true, t="MS+Buffs tab -> Spec Database section -> click Gather All Specs." },
        { a=true, t="The addon inspects each raid member in sequence. Results are cached and persist across sessions." },
        { a=true, t="For Feral Druids it distinguishes Bear vs Cat via the Protector of the Pack talent." },
        { q=true, t="What specs are recognised for MS Changes?" },
        { a=true, t="All 10 classes fully supported. Each class validates against its own spec list." },
        { a=true, t="DK: Blood Tank, Blood DPS, Frost, Unholy  |  Druid: Balance, Feral Cat, Feral Bear, Restoration" },
        { a=true, t="Warrior: Arms, Fury, Protection  |  Paladin: Holy, Protection, Retribution" },
        { a=true, t="Priest: Discipline, Holy, Shadow  |  Shaman: Elemental, Enhancement, Restoration" },
        { a=true, t="Rogue: Assassination, Combat, Subtlety  |  Mage: Arcane, Fire, Frost" },
        { a=true, t="Warlock: Affliction, Demonology, Destruction  |  Hunter: Beast Mastery, Marksmanship, Survival" },
        { s=true },
        { h=true, t="RAID BROWSER" },
        { q=true, t="What does Raid Browser do?" },
        { a=true, t="Detects LFM messages in Trade, General, World/Global and Yell channels automatically." },
        { a=true, t="Shows a live list: instance, GS required, roles needed, reserved items, leader name, age." },
        { q=true, t="How do I join a raid?" },
        { a=true, t="Click /W to whisper the leader (pre-fills your spec, GS, best achievement)." },
        { a=true, t="Click Join to send the whisper automatically." },
        { q=true, t="How do I set up my gear info?" },
        { a=true, t="Raid Browser tab -> Save Raid Gear section. Your spec is detected automatically from talents." },
        { a=true, t="GS is read from GearScoreLite if installed. Otherwise enter it manually and click Save." },
        { a=true, t="The /W and Join buttons auto-fill: inv 6.2k Retribution Paladin [Achievement]." },
        { q=true, t="What does the Ress column show?" },
        { a=true, t="YES (green) = reserved items detected (e.g. BP/SFS-RESS, DV ress, CTS)." },
        { a=true, t="NO (red) = no reserved items detected.  Hover for the full reserved list." },
        { q=true, t="Click-to-sort headers?" },
        { a=true, t="Click any column header (Instance, GS, Roles, Leader, Age) to sort. Click again to reverse." },
        { q=true, t="What raids does it detect?" },
        { a=true, t="ICC 10/25 HC/NM, ICC 8/12 HC, RS 10/25 HC/NM, ToC/ToGC 10/25, VoA 10/25," },
        { a=true, t="Ulduar 10/25, Naxxramas 10/25, OS 10/25, EoE 10/25, Onyxia 10/25, MC, BT, Kara." },
        { q=true, t="Listings expire after how long?" },
        { a=true, t="Messages expire after 60 seconds of silence. Max 50 listings stored at once." },
        { s=true },
        { h=true, t="SLASH COMMANDS" },
        { a=true, t="/rnt  or  /rntools       ->  Open or close the main window." },
        { a=true, t="/rnt reload              ->  Reload BiS database and re-detect your faction." },
        { a=true, t="/rnt config  (or cfg)    ->  Open the Settings/Config window." },
        { a=true, t="/rnt qa                  ->  Toggle the Quick Access panel." },
        { a=true, t="/rnt miniml              ->  Toggle the Mini Roll Manager window." },
        { a=true, t="/rnt overlay             ->  Toggle the status bar visibility." },
        { a=true, t="/rnt scale 1.25          ->  Set UI scale (0.25 - 3.0)." },
        { a=true, t="/rnt help                ->  Print all slash commands to chat." },
        { a=true, t="/rndebug                 ->  Open the RNDebug error logger (requires RNDebug addon)." },
    }
    local totalH = 8
    for _, entry in ipairs(sections) do
        if entry.s then
            totalH = totalH + 10
        elseif entry.h then
            -- Section header: gold
            local fs = content:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            fs:SetPoint("TOPLEFT",  content, "TOPLEFT",  8, -totalH)
            fs:SetPoint("TOPRIGHT", content, "TOPRIGHT", -8, -totalH)
            fs:SetJustifyH("LEFT")
            fs:SetTextColor(unpack(C.gold))
            fs:SetText(entry.t)
            totalH = totalH + 22
        elseif entry.q then
            -- Question: green
            local fs = content:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
            fs:SetPoint("TOPLEFT",  content, "TOPLEFT",  10, -totalH)
            fs:SetPoint("TOPRIGHT", content, "TOPRIGHT", -8, -totalH)
            fs:SetJustifyH("LEFT")
            fs:SetNonSpaceWrap(true)
            fs:SetTextColor(0.20, 1.00, 0.40, 1)
            fs:SetText("Q:  " .. entry.t)
            local fh = fs:GetHeight()
            totalH = totalH + (fh and fh > 16 and fh + 2 or 16)
        elseif entry.a then
            -- Answer: red/orange
            local fs = content:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
            fs:SetPoint("TOPLEFT",  content, "TOPLEFT",  22, -totalH)
            fs:SetPoint("TOPRIGHT", content, "TOPRIGHT", -8, -totalH)
            fs:SetJustifyH("LEFT")
            fs:SetNonSpaceWrap(true)
            fs:SetTextColor(1.00, 0.45, 0.15, 1)
            fs:SetText(entry.t)
            local fh = fs:GetHeight()
            totalH = totalH + (fh and fh > 16 and fh + 2 or 16)
        end
    end
    content:SetHeight(totalH + 20)

    return f
end


-------------------------------------------------------------------------------
function CreateBisTabContent(parent, onClose)
    if not BiSRaidDB then BiSRaidDB = {} end
    if not BiSRaidDB.items then
        BiSRaidDB.items = {}
        if type(BiSRaid_DefaultData) == "table" then
            for k, v in pairs(BiSRaid_DefaultData) do BiSRaidDB.items[k] = v end
        end
    end
    if not BiSRaidConfig then BiSRaidConfig = {} end
    if BiSRaidConfig.enabled == nil then BiSRaidConfig.enabled = true end

    -- Faction detection: Alliance sees Death's Verdict, Horde sees Death's Choice
    local faction = UnitFactionGroup("player")
    if BiSRaidDB and BiSRaidDB.items then
        if faction == "Alliance" then
            BiSRaidDB.items["death's choice"] = nil
        elseif faction == "Horde" then
            BiSRaidDB.items["death's verdict"] = nil
        end
    end

    local f = CreateFrame("Frame", nil, parent)
    f:SetAllPoints(parent)
    f:Hide()
    bisTabFrame = f
    SetBG(f, unpack(C.bg))

    -- ── Inner tab bar (Items / About / How To Use) — AceGUI TabGroup ───
    local innerTabs = AceGUI:Create("TabGroup")
    innerTabs:SetLayout("Fill")
    innerTabs.noAutoHeight = true
    innerTabs:SetTabs({
        { value = "items", text = "Item Browser" },
        { value = "about", text = "About" },
        { value = "info",  text = "How To Use" },
    })
    innerTabs.frame:SetParent(f)
    innerTabs.frame:ClearAllPoints()
    innerTabs.frame:SetAllPoints(f)
    innerTabs.frame:Show()
    -- Drop the TabGroup's own inset box/backdrop: f already supplies the
    -- single box+backdrop for this module, so the sub-tab content should
    -- sit directly inside it rather than inside a second nested box.
    innerTabs.border:SetBackdrop(nil)

    -- Enable ON/OFF toggle, overlaid in top-right corner of the tab bar
    local toggleBox = AceGUI:Create("CheckBox")
    toggleBox:SetLabel("BiS Announcements")
    toggleBox:SetWidth(170)
    toggleBox.frame:SetParent(innerTabs.frame)
    toggleBox.frame:ClearAllPoints()
    toggleBox.frame:SetPoint("TOPRIGHT", innerTabs.frame, "TOPRIGHT", -8, -2)
    toggleBox.frame:SetFrameLevel(innerTabs.frame:GetFrameLevel() + 10)
    toggleBox.frame:Show()
    local function UpdateToggle()
        toggleBox:SetValue(BiSRaidConfig.enabled == true)
    end
    -- Expose globally so Quick Access toggle can refresh this widget
    RNTools_RefreshBiSToggleBtn = UpdateToggle
    UpdateToggle()
    toggleBox:SetCallback("OnValueChanged", function(widget, event, value)
        BiSRaidConfig.enabled = value
        UpdateToggle()
        if RefreshStatusOverlay then RefreshStatusOverlay() end
    end)

    -- ── Content area below tab bar ────────────────────────────────────
    local contentArea = innerTabs.content

    -- ── ITEMS PANEL ───────────────────────────────────────────────────
    local itemsPanel = CreateFrame("Frame", nil, contentArea)
    itemsPanel:SetAllPoints(contentArea)

    -- Search row (AceGUI SimpleGroup, anchored to top of itemsPanel)
    local searchGroup = AceGUI:Create("SimpleGroup")
    searchGroup:SetLayout("Flow")
    searchGroup.frame:SetParent(itemsPanel)
    searchGroup.frame:ClearAllPoints()
    searchGroup.frame:SetPoint("TOPLEFT",  itemsPanel, "TOPLEFT",  0, 0)
    searchGroup.frame:SetPoint("TOPRIGHT", itemsPanel, "TOPRIGHT", 0, 0)
    searchGroup.frame:SetHeight(34)
    searchGroup.frame:Show()

    local sLabel = AceGUI:Create("Label")
    sLabel:SetText("Search:")
    sLabel:SetWidth(60)
    searchGroup:AddChild(sLabel)

    local searchBox = AceGUI:Create("EditBox")
    searchBox:SetLabel("")
    searchBox:SetWidth(220)
    searchBox:DisableButton(true)
    searchGroup:AddChild(searchBox)
    -- Debounce: typing fast queues many full-list rebuilds, which is heavy
    -- for ~135 widgets. Only the last keystroke within the delay actually
    -- triggers a refresh.
    local searchRefreshToken = 0
    local function QueueSearchRefresh()
        searchRefreshToken = searchRefreshToken + 1
        local token = searchRefreshToken
        After(0.2, function()
            if token == searchRefreshToken then RNTools_BisRefreshUI() end
        end)
    end
    searchBox:SetCallback("OnTextChanged", function(widget, event, text)
        searchFilter = text:lower(); selectedItem = nil
        QueueSearchRefresh()
    end)
    searchBox.editbox:SetScript("OnEscapePressed", function()
        searchBox:SetText(""); searchFilter = ""
        QueueSearchRefresh(); AceGUI:ClearFocus()
    end)

    local countLbl = AceGUI:Create("Label")
    countLbl:SetWidth(140)
    searchGroup:AddChild(countLbl)
    f._countFS = countLbl

    -- Two-column body: item list (left) + editor / add new (right)
    local bodyGroup = AceGUI:Create("SimpleGroup")
    bodyGroup:SetLayout("Flow")
    bodyGroup.frame:SetParent(itemsPanel)
    bodyGroup.frame:ClearAllPoints()
    bodyGroup.frame:SetPoint("TOPLEFT",     itemsPanel, "TOPLEFT",     0, -36)
    bodyGroup.frame:SetPoint("BOTTOMRIGHT", itemsPanel, "BOTTOMRIGHT", 0, 22)
    bodyGroup.frame:Show()

    -- Left: item list
    local leftGroup = AceGUI:Create("InlineGroup")
    leftGroup:SetTitle("Items")
    leftGroup:SetLayout("Fill")
    leftGroup:SetRelativeWidth(0.36)
    leftGroup:SetFullHeight(true)
    leftGroup.noAutoHeight = true
    bodyGroup:AddChild(leftGroup)

    local itemScroll = AceGUI:Create("ScrollFrame")
    itemScroll:SetLayout("List")
    leftGroup:AddChild(itemScroll)
    f._itemScroll = itemScroll

    -- Right: editor / add new
    local rightGroup = AceGUI:Create("InlineGroup")
    rightGroup:SetTitle("Editor")
    rightGroup:SetLayout("List")
    rightGroup:SetRelativeWidth(0.62)
    rightGroup:SetFullHeight(true)
    rightGroup.noAutoHeight = true
    bodyGroup:AddChild(rightGroup)

    local edHdr = AceGUI:Create("Label")
    edHdr:SetFullWidth(true)
    edHdr:SetText("Select an item to edit")
    edHdr:SetColor(unpack(C.title))
    rightGroup:AddChild(edHdr)
    f._edHdr = edHdr

    local edItemName = AceGUI:Create("Label")
    edItemName:SetFullWidth(true)
    edItemName:SetText("")
    rightGroup:AddChild(edItemName)
    f._edItemName = edItemName

    local bisBox = AceGUI:Create("EditBox")
    bisBox:SetLabel("BiS Specs (e.g. Fury Warrior / Ret Paladin):")
    bisBox:SetFullWidth(true)
    bisBox:DisableButton(true)
    rightGroup:AddChild(bisBox)
    f._bisBox = bisBox

    local altBox = AceGUI:Create("EditBox")
    altBox:SetLabel("Alternative Specs:")
    altBox:SetFullWidth(true)
    altBox:DisableButton(true)
    rightGroup:AddChild(altBox)
    f._altBox = altBox

    for _, box in ipairs({ bisBox, altBox }) do
        box.editbox:SetScript("OnEditFocusGained", function(self)
            if self:GetText() == "(None)" then
                self:SetText("")
                self:SetTextColor(0.90, 0.90, 0.90, 1)
            end
        end)
        box.editbox:SetScript("OnEditFocusLost", function(self)
            if self:GetText() == "" then
                self:SetText("(None)")
                self:SetTextColor(0.4, 0.4, 0.4, 1)
            end
        end)
    end

    local editBtnRow = AceGUI:Create("SimpleGroup")
    editBtnRow:SetLayout("Flow")
    editBtnRow:SetFullWidth(true)
    rightGroup:AddChild(editBtnRow)

    local saveBtn = AceGUI:Create("Button")
    saveBtn:SetText("Save Changes")
    saveBtn:SetWidth(140)
    editBtnRow:AddChild(saveBtn)

    local delBtn = AceGUI:Create("Button")
    delBtn:SetText("|cffff4d4dDelete Item|r")
    delBtn:SetWidth(140)
    editBtnRow:AddChild(delBtn)
    f._saveBtn = saveBtn; f._delBtn = delBtn

    local statusFS = AceGUI:Create("Label")
    statusFS:SetFullWidth(true)
    rightGroup:AddChild(statusFS)
    f._statusFS = statusFS

    local divider = AceGUI:Create("Heading")
    divider:SetText("")
    divider:SetFullWidth(true)
    rightGroup:AddChild(divider)

    local addHdr = AceGUI:Create("Label")
    addHdr:SetFullWidth(true)
    addHdr:SetText("Add New Item")
    addHdr:SetColor(unpack(C.title))
    rightGroup:AddChild(addHdr)

    local addHint = AceGUI:Create("Label")
    addHint:SetFullWidth(true)
    addHint:SetText("|cffFFD100Tip:|r Click the Item Name box below, then Shift+Click any item")
    rightGroup:AddChild(addHint)

    local nnBox = AceGUI:Create("EditBox")
    nnBox:SetLabel("Item Name:")
    nnBox:SetFullWidth(true)
    nnBox:DisableButton(true)
    rightGroup:AddChild(nnBox)
    nnBox.editbox:SetScript("OnEditFocusGained", function() nnBoxIsLinkTarget = true end)
    nnBox.editbox:SetScript("OnEditFocusLost",   function() nnBoxIsLinkTarget = false end)
    nnBox.editbox:SetScript("OnMouseDown",       function() nnBoxIsLinkTarget = true; nnBox.editbox:SetFocus() end)
    nnBox.editbox:SetScript("OnHide",            function() nnBoxIsLinkTarget = false end)
    nnBox:SetCallback("OnTextChanged", function(widget, event, txt)
        local itemName = (txt or ""):match("%[(.-)%]")
        if itemName then
            g_pendingItemID = 0; g_pendingDisplayName = ""
            nnBox:SetText(itemName:lower())
            nnBox.editbox:SetCursorPosition(string.len(itemName))
        end
    end)
    f._newName = nnBox; g_nnBox = nnBox

    local nbBox = AceGUI:Create("EditBox")
    nbBox:SetLabel("BiS Specs:")
    nbBox:SetFullWidth(true)
    nbBox:DisableButton(true)
    rightGroup:AddChild(nbBox)
    f._newBis = nbBox

    local naBox = AceGUI:Create("EditBox")
    naBox:SetLabel("Alt Specs (optional):")
    naBox:SetFullWidth(true)
    naBox:DisableButton(true)
    rightGroup:AddChild(naBox)
    f._newAlt = naBox

    local addBtnRow = AceGUI:Create("SimpleGroup")
    addBtnRow:SetLayout("Flow")
    addBtnRow:SetFullWidth(true)
    rightGroup:AddChild(addBtnRow)

    local addBtn = AceGUI:Create("Button")
    addBtn:SetText("Add Item")
    addBtn:SetWidth(140)
    addBtnRow:AddChild(addBtn)

    local rstBtn = AceGUI:Create("Button")
    rstBtn:SetText("|cffff9900Restore Defaults|r")
    rstBtn:SetWidth(160)
    addBtnRow:AddChild(rstBtn)

    -- Total item count (bottom-left footer)
    local totFS = AceGUI:Create("Label")
    totFS.frame:SetParent(itemsPanel)
    totFS.frame:ClearAllPoints()
    totFS.frame:SetPoint("BOTTOMLEFT",  itemsPanel, "BOTTOMLEFT",  4, 2)
    totFS.frame:SetPoint("BOTTOMRIGHT", itemsPanel, "BOTTOMRIGHT", -4, 2)
    totFS.frame:SetHeight(16)
    totFS.frame:Show()
    totFS:SetColor(unpack(C.dim))
    f._totFS = totFS

    -- ── About and How To panels ───────────────────────────────────────
    local aboutPanel  = BuildAboutPanel(contentArea)
    local howToPanel  = BuildHowToPanel(contentArea)

    -- ── Sub-tab switching ─────────────────────────────────────────────
    local function ShowInnerTab(name)
        itemsPanel:Hide(); aboutPanel:Hide(); howToPanel:Hide()
        if name == "items" then
            itemsPanel:Show()
            if not f._rowsBuilt then RNTools_BisRefreshUI() end
        elseif name == "about" then
            aboutPanel:Show()
        elseif name == "info" then
            howToPanel:Show()
        end
    end

    innerTabs:SetCallback("OnGroupSelected", function(_, _, value) ShowInnerTab(value) end)
    innerTabs:SelectTab("items")

    -- ── Button logic ──────────────────────────────────────────────────
    saveBtn:SetCallback("OnClick", function()
        if not selectedItem then statusFS:SetText("|cffff4444No item selected|r"); return end
        local entry = BiSRaidDB and BiSRaidDB.items and BiSRaidDB.items[selectedItem]
        if not entry then statusFS:SetText("|cffff4444Item not found|r"); return end
        local rawBIS = f._bisBox:GetText():match("^%s*(.-)%s*$") or ""
        local rawALT = f._altBox:GetText():match("^%s*(.-)%s*$") or ""
        -- Strip "(None)" placeholder text before saving
        if rawBIS == "(None)" or rawBIS == "none" then rawBIS = "" end
        if rawALT == "(None)" or rawALT == "none" then rawALT = "" end
        if rawBIS == "" and rawALT == "" then
            statusFS:SetText("|cffff4444At least BiS or Alt must be filled|r"); return
        end
        local bisTable, normBIS = ParseSpecText(rawBIS)
        local altTable, normALT = ParseSpecText(rawALT)
        entry.bis = bisTable; entry.alt = altTable
        entry.bis_text = normBIS; entry.alt_text = normALT
        f._bisBox:SetText(normBIS); f._altBox:SetText(normALT)
        statusFS:SetText("|cff20ff60Saved!|r"); RNTools_BisRefreshUI()
    end)

    delBtn:SetCallback("OnClick", function()
        if not selectedItem then statusFS:SetText("|cffff4444No item selected|r"); return end
        if delBtn._confirm then
            BiSRaidDB.items[selectedItem] = nil; selectedItem = nil; delBtn._confirm = nil
            delBtn:SetText("|cffff4d4dDelete Item|r")
            edHdr:SetText("Select an item to edit"); edItemName:SetText("")
            f._bisBox:SetText(""); f._altBox:SetText("")
            statusFS:SetText("|cffff4444Deleted|r"); RNTools_BisRefreshUI()
        else
            delBtn._confirm = true
            delBtn:SetText("|cffff8000Confirm Delete|r")
            After(3, function()
                if delBtn._confirm then
                    delBtn._confirm = nil
                    delBtn:SetText("|cffff4d4dDelete Item|r")
                end
            end)
        end
    end)

    addBtn:SetCallback("OnClick", function()
        local rawNameInput = f._newName:GetText():match("^%s*(.-)%s*$") or ""
        -- If user pasted a hyperlink, extract the item name from it
        local linkExtract = rawNameInput:match("%[(.-)%]")
        local name = linkExtract and linkExtract:lower() or rawNameInput:lower()
        local rawBIS = f._newBis:GetText():match("^%s*(.-)%s*$") or ""
        local rawALT = f._newAlt:GetText():match("^%s*(.-)%s*$") or ""
        if rawBIS == "(None)" then rawBIS = "" end
        if rawALT == "(None)" then rawALT = "" end
        if name == "" then statusFS:SetText("|cffff4444Name required|r"); return end
        if rawBIS == "" and rawALT == "" then statusFS:SetText("|cffff4444BiS or Alt required|r"); return end
        if BiSRaidDB.items[name] then statusFS:SetText("|cffff4444Already exists|r"); return end
        local bisTable, normBIS = ParseSpecText(rawBIS)
        local altTable, normALT = ParseSpecText(rawALT)
        local itemID = g_pendingItemID or 0; local displayName = g_pendingDisplayName or ""
        local itemLink = g_pendingLink or ""
        g_pendingItemID = 0; g_pendingDisplayName = ""; g_pendingLink = ""
        if displayName == "" and itemID > 0 then local n = GetItemInfo(itemID); if n then displayName = n end end
        if displayName == "" then displayName = name:gsub("(%a)([%w_']*)", function(a,b) return a:upper()..b end) end
        BiSRaidDB.items[name] = { name=name, display=displayName, item_id=itemID, item_link=itemLink, bis=bisTable, alt=altTable, bis_text=normBIS, alt_text=normALT }
        iconQueried[name] = nil; RequestIcon(name, itemID)
        f._newName:SetText(""); f._newBis:SetText(""); f._newAlt:SetText("")
        selectedItem = name; statusFS:SetText("|cff20ff60Item added!|r")
        RNTools_BisRefreshUI(); RNTools_BisLoadEditor(name)
    end)

    rstBtn._confirm = nil
    rstBtn:SetCallback("OnClick", function()
        if rstBtn._confirm then
            rstBtn._confirm = nil
            rstBtn:SetText("|cffff9900Restore Defaults|r")
            BiSRaidDB.items = {}
            if type(BiSRaid_DefaultData) == "table" then
                for k, v in pairs(BiSRaid_DefaultData) do BiSRaidDB.items[k] = v end
            end
            iconCache = {}; iconPending = {}; iconQueried = {}
            selectedItem = nil; RNTools_BisLoadEditor(nil)
            statusFS:SetText("|cff20ff60Defaults restored!|r"); RNTools_BisRefreshUI()
        else
            rstBtn._confirm = true
            rstBtn:SetText("|cffff4d4dClick again to CONFIRM|r")
            After(4, function()
                if rstBtn._confirm then
                    rstBtn._confirm = nil
                    rstBtn:SetText("|cffff9900Restore Defaults|r")
                end
            end)
        end
    end)

    f:SetScript("OnShow", function()
        local w, h = f:GetWidth(), f:GetHeight()
        if w and w > 10 then innerTabs:SetWidth(w) end
        if h and h > 10 then innerTabs:SetHeight(h) end
        searchGroup:DoLayout()
        bodyGroup:DoLayout()
        -- Deferred re-layout one tick later: anchor-derived widths (itemsPanel,
        -- bodyGroup) are not guaranteed to resolve synchronously on first Show.
        -- We explicitly push the resolved width into bodyGroup via the AceGUI
        -- SetWidth method (which updates content.width via OnWidthSet) so that
        -- SetRelativeWidth(0.36/0.62) on leftGroup/rightGroup computes against
        -- the real ~1000px column width instead of SimpleGroup's 300px default.
        C_Timer_fake(0, function()
            local pw = itemsPanel:GetWidth()
            if pw and pw > 10 then
                searchGroup:SetWidth(pw)
                bodyGroup:SetWidth(pw)
            end
            searchGroup:DoLayout()
            bodyGroup:DoLayout()
        end)
        if not f._rowsBuilt then RNTools_BisRefreshUI() end
    end)
    return f
end

-- Global toggle for Quick Access window
function RNTools_ToggleBiSTracker()
    if BiSRaidConfig then
        BiSRaidConfig.enabled = not (BiSRaidConfig.enabled == true)
        -- Refresh the toggle button inside the BiS tab
        if RNTools_RefreshBiSToggleBtn then RNTools_RefreshBiSToggleBtn() end
        if RefreshStatusOverlay then RefreshStatusOverlay() end
    end
end
