local addonName, TOAST = ...

local FRAME_W  = 745
local BANNER_H = 100
local LABELS_H = 20
local ROW_H    = 26

-- Table viewport sizing: derive the frame/scrollframe height from named
-- constants so exactly VISIBLE_ROWS rows fit with no partial row, instead of
-- an independently eyeballed FRAME_H.
local SEARCH_AREA_HEIGHT    = 46  -- header bottom -> table header top (search box + spacing)
local TABLE_HEADER_GAP      = 3   -- grey separator (1px) + 2px clearance before the scrollframe
local VISIBLE_ROWS          = 20
local TABLE_VIEWPORT_HEIGHT = ROW_H * VISIBLE_ROWS
local BOTTOM_PADDING        = 20  -- clearance below the scrollframe for the backdrop border

local SF_TOP  = BANNER_H + SEARCH_AREA_HEIGHT + LABELS_H + TABLE_HEADER_GAP
local FRAME_H = SF_TOP + TABLE_VIEWPORT_HEIGHT + BOTTOM_PADDING

local GOLD = { 0.88, 0.75, 0.36 }
local TEAL = { 0, 0.92, 0.79 }  -- #00EAC9
local BORDER = { 1, 1, 1, 0.12 }

local COLS = {
    { label = "#",          width = 28,  xOff = 4,   align = "RIGHT"  },
    { label = "Discord",    width = 140, xOff = 36,  align = "LEFT",
      -- sub-offsets within the Discord column, shared by header + row rendering:
      statusOff = 0,   -- online/known status dot
      iconOff   = 8,   -- prefix icon (or plain name when there's no prefix)
      labelOff  = 28,  -- prefix label text (e.g. "TIDE")
      nameOff   = 62,  -- discord display name
    },
    { label = "Char-Realm", width = 150, xOff = 180, align = "LEFT"   },
    { label = "Role",       width = 50,  xOff = 334, align = "LEFT"   },
    { label = "RIO",  width = 55, xOff = 388, align = "RIGHT"  },
    { label = "Resi", width = 90, xOff = 468, align = "CENTER",
      headerTex = "Interface\\AddOns\\TOAST\\media\\discord\\resi_256x128.tga", texW = 96, texH = 48 },
    { label = "Key",  width = 80, xOff = 576, align = "CENTER" },
}

local CLASS_TOKEN = {
    ["Death Knight"] = "DEATHKNIGHT",
    ["Demon Hunter"] = "DEMONHUNTER",
    ["Druid"]        = "DRUID",
    ["Evoker"]       = "EVOKER",
    ["Hunter"]       = "HUNTER",
    ["Mage"]         = "MAGE",
    ["Monk"]         = "MONK",
    ["Paladin"]      = "PALADIN",
    ["Priest"]       = "PRIEST",
    ["Rogue"]        = "ROGUE",
    ["Shaman"]       = "SHAMAN",
    ["Warlock"]      = "WARLOCK",
    ["Warrior"]      = "WARRIOR",
}

local PREFIX_MAP = {
    ["🌊ᴛɪᴅᴇ"] = { tex = "Interface\\AddOns\\TOAST\\media\\prefixes\\tide", label = "TIDE" },
    ["⚡ᴛɪᴅᴇ"]  = { tex = "Interface\\AddOns\\TOAST\\media\\prefixes\\raid", label = "TIDE" },
    ["🔱ᴛɪᴅᴇ"] = { tex = "Interface\\AddOns\\TOAST\\media\\prefixes\\daan", label = "TIDE" },
    ["🧭ᴛɪᴅᴇ"] = { tex = "Interface\\AddOns\\TOAST\\media\\prefixes\\way",  label = "TIDE" },
    ["💦ꜱᴍɴ"]  = { tex = "Interface\\AddOns\\TOAST\\media\\prefixes\\smn",  label = "SMN"  },
    ["⚓ᴄᴘᴛ"]  = { tex = "Interface\\AddOns\\TOAST\\media\\prefixes\\cpt",  label = "CPT"  },
}

local ROLE_TEX = {
    HEAL = "Interface\\AddOns\\TOAST\\media\\roles\\heal_32.tga",
    DPS  = "Interface\\AddOns\\TOAST\\media\\roles\\dps_32.tga",
    TANK = "Interface\\AddOns\\TOAST\\media\\roles\\tank_32.tga",
}

TOAST.CLASS_TOKEN = CLASS_TOKEN
TOAST.PREFIX_MAP  = PREFIX_MAP
TOAST.ROLE_TEX    = ROLE_TEX

local function ParseDiscordPrefix(discordNickname)
    local prefix = discordNickname:match("^(%S+)")
    local p = prefix and PREFIX_MAP[prefix]
    local name = discordNickname:match("^%S+ (.+)$") or discordNickname
    return p, name
end
TOAST.ParseDiscordPrefix = ParseDiscordPrefix

local FRESH_THRESHOLD = 3600

local function GetAddonStatus(entry)
    -- Primair: C_Club native online check
    if TOAST._onlineMembers and TOAST._onlineMembers[entry.name] then
        return "online"
    end
    -- Fallback: lastSeen via comms
    local data = TOAST.keys and TOAST.keys[entry.name]
    if not data or not data.lastSeen then return nil end
    local age = GetServerTime() - data.lastSeen
    if age < FRESH_THRESHOLD then
        return "online"
    else
        return "known"
    end
end

local lb
local rows = {}

local function BuildLeaderboard()
    local memberLookup = {}
    for _, m in ipairs(TOAST.members) do
        memberLookup[m.name] = m
    end

    lb = CreateFrame("Frame", "TOASTLeaderboard", UIParent, "BackdropTemplate")
    Mixin(lb, BackdropTemplateMixin)
    lb:SetSize(FRAME_W, FRAME_H)
    lb:SetPoint("CENTER")
    lb:SetMovable(true)
    lb:SetClampedToScreen(true)
    lb:SetFrameStrata("HIGH")
    lb:Hide()

    local bg = lb:CreateTexture(nil, "BACKGROUND")
    bg:SetAllPoints()
    bg:SetColorTexture(0.06, 0.08, 0.10, 1.0)

    local borderTop = lb:CreateTexture(nil, "BORDER")
    borderTop:SetColorTexture(BORDER[1], BORDER[2], BORDER[3], BORDER[4])
    borderTop:SetPoint("TOPLEFT", lb, "TOPLEFT", 0, 0)
    borderTop:SetPoint("TOPRIGHT", lb, "TOPRIGHT", 0, 0)
    borderTop:SetHeight(1)

    local borderBottom = lb:CreateTexture(nil, "BORDER")
    borderBottom:SetColorTexture(BORDER[1], BORDER[2], BORDER[3], BORDER[4])
    borderBottom:SetPoint("BOTTOMLEFT", lb, "BOTTOMLEFT", 0, 0)
    borderBottom:SetPoint("BOTTOMRIGHT", lb, "BOTTOMRIGHT", 0, 0)
    borderBottom:SetHeight(1)

    local borderLeft = lb:CreateTexture(nil, "BORDER")
    borderLeft:SetColorTexture(BORDER[1], BORDER[2], BORDER[3], BORDER[4])
    borderLeft:SetPoint("TOPLEFT", lb, "TOPLEFT", 0, 0)
    borderLeft:SetPoint("BOTTOMLEFT", lb, "BOTTOMLEFT", 0, 0)
    borderLeft:SetWidth(1)

    local borderRight = lb:CreateTexture(nil, "BORDER")
    borderRight:SetColorTexture(BORDER[1], BORDER[2], BORDER[3], BORDER[4])
    borderRight:SetPoint("TOPRIGHT", lb, "TOPRIGHT", 0, 0)
    borderRight:SetPoint("BOTTOMRIGHT", lb, "BOTTOMRIGHT", 0, 0)
    borderRight:SetWidth(1)

    -- ── Header banner ──────────────────────────────────────────────
    local header = CreateFrame("Frame", nil, lb)
    header:SetSize(FRAME_W, BANNER_H)
    header:SetPoint("TOPLEFT", lb, "TOPLEFT", 0, 0)
    header:EnableMouse(true)
    header:RegisterForDrag("LeftButton")
    header:SetScript("OnDragStart", function() lb:StartMoving() end)
    header:SetScript("OnDragStop",  function() lb:StopMovingOrSizing() end)

    -- Gold top accent, on its own frame leveled above the header so the
    -- tentacle banner (a header child frame, which outranks lb's own layers)
    -- can't draw over it and muddy the gold — same fix as the divider below.
    local topAccentFrame = CreateFrame("Frame", nil, lb)
    topAccentFrame:SetAllPoints(lb)
    topAccentFrame:SetFrameLevel(header:GetFrameLevel() + 1)

    local topAccent = topAccentFrame:CreateTexture(nil, "ARTWORK")
    topAccent:SetColorTexture(GOLD[1], GOLD[2], GOLD[3], 1)
    topAccent:SetPoint("TOPLEFT", lb, "TOPLEFT", 0, 0)
    topAccent:SetPoint("TOPRIGHT", lb, "TOPRIGHT", 0, 0)
    topAccent:SetHeight(2)

    -- Nautical tentacle artwork spanning the full header; the asset itself
    -- is dark/empty on the left and fades into tentacles on the right.
    local tentacle = header:CreateTexture(nil, "BACKGROUND")
    tentacle:SetTexture("Interface\\AddOns\\TOAST\\media\\banners\\tentakel.png")
    tentacle:SetTexCoord(0, 1, 0, 1)
    tentacle:SetAllPoints(header)
    tentacle:SetVertexColor(1, 1, 1, 0.48)

    local logo = header:CreateTexture(nil, "OVERLAY")
    logo:SetTexture("Interface\\AddOns\\TOAST\\media\\discord\\toast_128.tga")
    logo:SetSize(64, 64)
    logo:SetPoint("TOPLEFT", header, "TOPLEFT", 12, -8)

    local titleFS = header:CreateFontString(nil, "OVERLAY", "GameFontNormalHuge")
    titleFS:SetPoint("TOPLEFT", logo, "TOPRIGHT", 10, -8)
    titleFS:SetTextColor(GOLD[1], GOLD[2], GOLD[3])
    titleFS:SetText("TOAST LEADERBOARD")

    local subFS = header:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    subFS:SetPoint("TOPLEFT", titleFS, "BOTTOMLEFT", 0, -4)
    subFS:SetTextColor(1, 1, 1, 0.45)
    subFS:SetText("Season Rankings  \226\128\162  " .. #TOAST.leaderboard .. " members  \226\128\162  v" .. TOAST.VERSION)

    TOAST.CreateCloseButton(lb, function() TOAST.commands["lb"]() end)

    local backBtn = TOAST.CreateActionButton(lb, "Dashboard", "gold", 90, 22)
    backBtn:SetPoint("TOPRIGHT", lb, "TOPRIGHT", -36, -10)
    backBtn:SetFrameLevel(lb:GetFrameLevel() + 5)
    backBtn:SetScript("OnClick", function()
        TOAST.commands["lb"]()
        TOAST.commands["dashboard"]()
    end)

    -- Slightly more solid background so the tentacle banner behind it reads
    -- less busy (overrides just this button's own bg alpha, not the shared style).
    local backBtnBg = select(1, backBtn:GetRegions())
    if backBtnBg then
        backBtnBg:SetColorTexture(GOLD[1], GOLD[2], GOLD[3], 0.30)
        backBtn:SetScript("OnEnter", function() backBtnBg:SetColorTexture(GOLD[1], GOLD[2], GOLD[3], 0.42) end)
        backBtn:SetScript("OnLeave", function() backBtnBg:SetColorTexture(GOLD[1], GOLD[2], GOLD[3], 0.30) end)
    end

    -- ── Gold separator with anchor medallion (y = -BANNER_H) ───────
    -- Leveled above the header (options.aboveFrame), so the medallion (which
    -- overlaps the header's bottom edge) always draws on top of the tentacle
    -- banner instead of being covered by it.
    local dividerY = -(BANNER_H + 1)  -- vertical center of the old 2px line
    TOAST.CreateAnchorDivider(lb, 0, FRAME_W, dividerY, {
        medallionSize = 32,
        gap = 6,
        aboveFrame = header,
    })

    -- ── Search box (between gold sep and column labels) ───────────
    rows = {}
    local content  -- pre-declared so the OnTextChanged closure captures it as an upvalue
    local scrollToTop  -- pre-declared so the OnTextChanged closure captures it as an upvalue

    local searchContainer = CreateFrame("Frame", nil, lb)
    searchContainer:SetSize(200, 26)
    searchContainer:SetPoint("TOPRIGHT", lb, "TOPRIGHT", -10, -(BANNER_H + 12))

    local searchBg = searchContainer:CreateTexture(nil, "BACKGROUND")
    searchBg:SetAllPoints()
    searchBg:SetColorTexture(0.04, 0.05, 0.07, 0.8)

    local sBorderTop = searchContainer:CreateTexture(nil, "BORDER")
    sBorderTop:SetPoint("TOPLEFT", searchContainer, "TOPLEFT", 0, 0)
    sBorderTop:SetPoint("TOPRIGHT", searchContainer, "TOPRIGHT", 0, 0)
    sBorderTop:SetHeight(1)

    local sBorderBottom = searchContainer:CreateTexture(nil, "BORDER")
    sBorderBottom:SetPoint("BOTTOMLEFT", searchContainer, "BOTTOMLEFT", 0, 0)
    sBorderBottom:SetPoint("BOTTOMRIGHT", searchContainer, "BOTTOMRIGHT", 0, 0)
    sBorderBottom:SetHeight(1)

    local sBorderLeft = searchContainer:CreateTexture(nil, "BORDER")
    sBorderLeft:SetPoint("TOPLEFT", searchContainer, "TOPLEFT", 0, 0)
    sBorderLeft:SetPoint("BOTTOMLEFT", searchContainer, "BOTTOMLEFT", 0, 0)
    sBorderLeft:SetWidth(1)

    local sBorderRight = searchContainer:CreateTexture(nil, "BORDER")
    sBorderRight:SetPoint("TOPRIGHT", searchContainer, "TOPRIGHT", 0, 0)
    sBorderRight:SetPoint("BOTTOMRIGHT", searchContainer, "BOTTOMRIGHT", 0, 0)
    sBorderRight:SetWidth(1)

    local function SetSearchBorderAlpha(a)
        sBorderTop:SetColorTexture(1, 1, 1, a)
        sBorderBottom:SetColorTexture(1, 1, 1, a)
        sBorderLeft:SetColorTexture(1, 1, 1, a)
        sBorderRight:SetColorTexture(1, 1, 1, a)
    end
    SetSearchBorderAlpha(0.08)

    local searchBox = CreateFrame("EditBox", "TOASTLeaderboardSearch", searchContainer)
    searchBox:SetAllPoints()
    searchBox:SetFont("Fonts\\FRIZQT__.TTF", 12, "")
    searchBox:SetTextColor(1, 1, 1, 1)
    searchBox:SetTextInsets(10, 24, 0, 0)
    searchBox:SetAutoFocus(false)
    searchBox:SetMaxLetters(30)

    local searchPlaceholder = searchContainer:CreateFontString(nil, "OVERLAY")
    searchPlaceholder:SetFont("Fonts\\FRIZQT__.TTF", 12, "")
    searchPlaceholder:SetTextColor(1, 1, 1, 0.3)
    searchPlaceholder:SetPoint("LEFT", searchContainer, "LEFT", 10, 0)
    searchPlaceholder:SetText("Search name, dungeon, role...")

    local clearBtn = CreateFrame("Button", nil, searchContainer)
    clearBtn:SetSize(20, 20)
    clearBtn:SetPoint("RIGHT", searchContainer, "RIGHT", -2, 0)
    clearBtn:Hide()

    local clearLabel = clearBtn:CreateFontString(nil, "OVERLAY")
    clearLabel:SetFont("Fonts\\FRIZQT__.TTF", 14, "")
    clearLabel:SetPoint("CENTER")
    clearLabel:SetText("\195\151")
    clearLabel:SetTextColor(1, 1, 1, 0.35)

    clearBtn:SetScript("OnEnter", function() clearLabel:SetTextColor(1, 1, 1, 1) end)
    clearBtn:SetScript("OnLeave", function() clearLabel:SetTextColor(1, 1, 1, 0.35) end)
    clearBtn:SetScript("OnClick", function()
        searchBox:SetText("")
        searchBox:ClearFocus()
    end)

    searchBox:SetScript("OnEditFocusGained", function() SetSearchBorderAlpha(0.15) end)
    searchBox:SetScript("OnEditFocusLost", function() SetSearchBorderAlpha(0.08) end)
    searchBox:SetScript("OnTextChanged", function(self)
        if self:GetText() == "" then
            searchPlaceholder:Show()
            clearBtn:Hide()
        else
            searchPlaceholder:Hide()
            clearBtn:Show()
        end
        if not content then return end
        local query = self:GetText():lower()
        if query == "search name, dungeon, role..." then
            for i, r in ipairs(rows) do
                r.frame:Show()
                r.frame:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -(i - 1) * ROW_H)
            end
            content:SetHeight(#rows * ROW_H)
            if scrollToTop then scrollToTop() end
            return
        end
        local visibleIndex = 0
        for _, r in ipairs(rows) do
            local keyData = TOAST.keys and TOAST.keys[TOAST.leaderboard[r.index].name]
            local keyStr = keyData and ("+" .. keyData.level .. " " .. (keyData.short or "")):lower() or ""
            local match = (query == ""
                or r.name:find(query, 1, true)
                or r.discord:find(query, 1, true)
                or r.role:find(query, 1, true)
                or keyStr:find(query, 1, true)
                or r.realm:find(query, 1, true)
                or r.rio:find(query, 1, true))
            if match then
                r.frame:Show()
                r.frame:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -visibleIndex * ROW_H)
                visibleIndex = visibleIndex + 1
            else
                r.frame:Hide()
            end
        end
        content:SetHeight(math.max(visibleIndex * ROW_H, 1))
        if scrollToTop then scrollToTop() end
    end)

    -- ── Column labels (below search box) ───────────────────────────
    local colBar = CreateFrame("Frame", nil, lb)
    colBar:SetSize(FRAME_W - 20, LABELS_H)
    colBar:SetPoint("TOPLEFT", lb, "TOPLEFT", 10, -(BANNER_H + SEARCH_AREA_HEIGHT))
    -- The Resi header icon renders taller than this bar and overflows into
    -- the table area below; table rows nest 3 levels deep (sf > content >
    -- row) so they'd otherwise draw over that overflow. Keep colBar above it.
    colBar:SetFrameLevel(lb:GetFrameLevel() + 10)

    for _, col in ipairs(COLS) do
        if col.headerTex then
            local tex = colBar:CreateTexture(nil, "OVERLAY")
            tex:SetTexture(col.headerTex)
            tex:SetSize(col.texW, col.texH)
            tex:SetPoint("CENTER", colBar, "LEFT", col.xOff + col.width / 2, 0)
        else
            -- Columns with an iconOff (e.g. Discord) have their header aligned
            -- with the identity content (icon/name), not the status dot before it.
            local headerXOff = col.iconOff and (col.xOff + col.iconOff) or col.xOff
            local headerWidth = col.iconOff and (col.width - col.iconOff) or col.width

            local fs = colBar:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            fs:SetWidth(headerWidth)
            fs:SetJustifyH(col.align)
            fs:SetPoint("LEFT", colBar, "LEFT", headerXOff, 0)
            fs:SetTextColor(GOLD[1], GOLD[2], GOLD[3])
            fs:SetText(col.label)
        end
    end

    -- ── Grey separator below labels ────────────────────────────────
    local greySep = lb:CreateTexture(nil, "ARTWORK")
    greySep:SetColorTexture(0.5, 0.5, 0.5, 0.4)
    greySep:SetSize(FRAME_W, 1)
    greySep:SetPoint("TOPLEFT", lb, "TOPLEFT", 0, -(BANNER_H + SEARCH_AREA_HEIGHT + LABELS_H + 1))

    -- ── Scroll frame ───────────────────────────────────────────────
    -- Height comes directly from TABLE_VIEWPORT_HEIGHT (ROW_H * VISIBLE_ROWS),
    -- not back-derived from FRAME_H, so the two can never drift out of sync.
    local sf = CreateFrame("ScrollFrame", "TOASTLeaderboardSF", lb)
    sf:SetSize(FRAME_W - 54, TABLE_VIEWPORT_HEIGHT)
    sf:SetPoint("TOPLEFT", lb, "TOPLEFT", 10, -SF_TOP)

    content = CreateFrame("Frame", nil, sf)
    content:SetSize(FRAME_W - 54, #TOAST.leaderboard * ROW_H)
    sf:SetScrollChild(content)

    _, _, scrollToTop = TOAST.CreateCustomScrollbar(sf, content)

    local playerName = UnitName("player")

    for i, entry in ipairs(TOAST.leaderboard) do
        local row = CreateFrame("Frame", nil, content)
        row:SetSize(FRAME_W - 54, ROW_H)
        row:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -(i - 1) * ROW_H)

        local isEven = (i % 2 == 0)
        local bg = row:CreateTexture(nil, "BACKGROUND")
        bg:SetAllPoints()
        if isEven then
            bg:SetColorTexture(0.1, 0.1, 0.3, 0.15)
        else
            bg:SetColorTexture(0, 0, 0, 0)
        end

        local isSelf = (entry.name == playerName)
        if isSelf then
            bg:SetColorTexture(GOLD[1], GOLD[2], GOLD[3], 0.12)
            local border = row:CreateTexture(nil, "OVERLAY")
            border:SetColorTexture(GOLD[1], GOLD[2], GOLD[3], 0.6)
            border:SetSize(FRAME_W - 54, 1)
            border:SetPoint("TOPLEFT", row, "TOPLEFT", 0, 0)
            local border2 = row:CreateTexture(nil, "OVERLAY")
            border2:SetColorTexture(GOLD[1], GOLD[2], GOLD[3], 0.6)
            border2:SetSize(FRAME_W - 54, 1)
            border2:SetPoint("BOTTOMLEFT", row, "BOTTOMLEFT", 0, 0)
        end

        row:EnableMouse(true)
        row:SetScript("OnEnter", function()
            bg:SetColorTexture(0.11, 0.13, 0.15, 0.50)
        end)
        row:SetScript("OnLeave", function()
            if isSelf then
                bg:SetColorTexture(GOLD[1], GOLD[2], GOLD[3], 0.12)
            elseif isEven then
                bg:SetColorTexture(0.1, 0.1, 0.3, 0.15)
            else
                bg:SetColorTexture(0, 0, 0, 0)
            end
        end)

        row:SetScript("OnMouseUp", function(self, button)
            if button ~= "RightButton" then return end
            local target = entry.name .. "-" .. entry.realm:gsub(" ", "")
            MenuUtil.CreateContextMenu(self, function(ownerRegion, rootDescription)
                local memberEntry = memberLookup[entry.name]
                if memberEntry and memberEntry.discord_id then
                    local crewCount = 0
                    for _, m in ipairs(TOAST.members) do
                        if m.discord_id == memberEntry.discord_id then
                            crewCount = crewCount + 1
                        end
                    end
                    if crewCount > 1 then
                        rootDescription:CreateButton("View Crew", function()
                            TOAST.ShowCrewPopup(memberEntry.discord_id)
                        end)
                    end
                end
                rootDescription:CreateButton("Copy Name", function()
                    TOAST.ShowCopyPopup(target)
                end)
                rootDescription:CreateButton("Invite", function()
                    C_PartyInfo.InviteUnit(target)
                end)
                rootDescription:CreateButton("Whisper", function()
                    ChatFrame_OpenChat("/w " .. target .. " ", DEFAULT_CHAT_FRAME)
                end)
            end)
        end)

        -- Class-colored name, realm in the same muted grey as the prefix label.
        local token = CLASS_TOKEN[entry.class_]
        local c = token and RAID_CLASS_COLORS[token]
        local charRealm
        if c then
            charRealm = string.format("|cff%02x%02x%02x%s|r|cffCCCCCC-%s|r",
                math.floor(c.r * 255), math.floor(c.g * 255), math.floor(c.b * 255),
                entry.name, entry.realm)
        else
            charRealm = entry.name .. "|cffCCCCCC-" .. entry.realm .. "|r"
        end

        local keyData = TOAST.keys and TOAST.keys[entry.name]
        local keyVal  = keyData and ("+" .. keyData.level .. " " .. keyData.short) or "\226\128\148"

        local vals = {
            entry.rank,
            "",  -- ci==2 handled below via PREFIX_MAP
            charRealm,
            entry.role,
            entry.rio,
            entry.resi,
            keyVal,
        }

        local keyFS
        local dotTex
        for ci, col in ipairs(COLS) do
            if ci == 2 then
                local p, name = ParseDiscordPrefix(entry.discord_nickname)
                local memberEntry = memberLookup[entry.name]

                local dot = row:CreateTexture(nil, "OVERLAY")
                dot:SetSize(6, 6)
                dot:SetPoint("LEFT", row, "LEFT", col.xOff + col.statusOff, 0)
                local status = GetAddonStatus(entry)
                if status == "online" then
                    dot:SetColorTexture(TEAL[1], TEAL[2], TEAL[3], 1)
                elseif status == "known" then
                    dot:SetColorTexture(0.5, 0.5, 0.5, 0.6)
                else
                    dot:Hide()
                end
                dotTex = dot

                if p then
                    local icon = row:CreateTexture(nil, "OVERLAY")
                    icon:SetTexture(p.tex)
                    icon:SetSize(20, 20)
                    icon:SetPoint("LEFT", row, "LEFT", col.xOff + col.iconOff, 0)

                    local labelFS = row:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
                    labelFS:SetPoint("LEFT", row, "LEFT", col.xOff + col.labelOff, 0)
                    labelFS:SetText(p.label)
                    labelFS:SetTextColor(0.8, 0.8, 0.8)       -- #CCCCCC

                    local nameFS = row:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
                    nameFS:SetWidth(col.width - col.nameOff)
                    nameFS:SetJustifyH("LEFT")
                    nameFS:SetPoint("LEFT", row, "LEFT", col.xOff + col.nameOff, 0)
                    nameFS:SetText(name)
                else
                    local fs = row:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
                    fs:SetWidth(col.width - col.iconOff)
                    fs:SetJustifyH(col.align)
                    fs:SetPoint("LEFT", row, "LEFT", col.xOff + col.iconOff, 0)
                    fs:SetText(name)
                end
            elseif ci == 4 then
                local tex = row:CreateTexture(nil, "OVERLAY")
                tex:SetTexture(ROLE_TEX[entry.role] or "")
                tex:SetSize(22, 22)
                tex:SetPoint("LEFT", row, "LEFT", col.xOff, 0)
            else
                local fs = row:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
                fs:SetWidth(col.width)
                fs:SetJustifyH(col.align)
                fs:SetPoint("LEFT", row, "LEFT", col.xOff, 0)
                fs:SetText(tostring(vals[ci]))
                if ci == 7 then keyFS = fs end
            end
        end
        rows[i] = { frame = row, name = entry.name:lower(), discord = entry.discord_nickname:lower(),
                    role = entry.role:lower(), realm = entry.realm:lower(), rio = tostring(entry.rio),
                    index = i, keyFS = keyFS, dotTex = dotTex }
    end
    content:SetHeight(#TOAST.leaderboard * ROW_H)
end

TOAST.RefreshKeyColumn = function()
    if not lb then return end
    for i, r in ipairs(rows) do
        local entry   = TOAST.leaderboard[i]
        local keyData = TOAST.keys and TOAST.keys[entry.name]
        r.keyFS:SetText(keyData and ("+" .. keyData.level .. " " .. keyData.short) or "\226\128\148")
    end
    TOAST.RefreshStatusDots()
end

TOAST.RefreshStatusDots = function()
    TOAST._onlineMembers = TOAST.GetOnlineMembers and TOAST.GetOnlineMembers() or nil
    for i, r in ipairs(rows) do
        local entry
        for _, e in ipairs(TOAST.leaderboard) do
            if e.name:lower() == r.name then
                entry = e
                break
            end
        end
        if entry and r.dotTex then
            local status = GetAddonStatus(entry)
            if status == "online" then
                r.dotTex:SetColorTexture(TEAL[1], TEAL[2], TEAL[3], 1)
                r.dotTex:Show()
            elseif status == "known" then
                r.dotTex:SetColorTexture(0.5, 0.5, 0.5, 0.6)
                r.dotTex:Show()
            else
                r.dotTex:Hide()
            end
        end
    end
end

TOAST.commands["lb"] = function()
    if not lb then BuildLeaderboard() end
    if lb:IsShown() then
        lb:Hide()
    else
        lb:Show()
        if TOAST.RefreshUnknowns then TOAST.RefreshUnknowns() end
        TOAST.RefreshStatusDots()
    end
end
