local _, ns = ...
local MythicTools = ns.MythicTools

local WHITE_TEXTURE = "Interface\\Buttons\\WHITE8x8"
local CLASS_CIRCLE_TEXTURE = "Interface\\TargetingFrame\\UI-Classes-Circles"
local QUESTION_MARK_ICON = "Interface\\ICONS\\INV_Misc_QuestionMark"
local ADDON_ICON = "Interface\\AddOns\\SkyMythicHistory\\Media\\smh.tga"
local SORT_ARROW_ICON = "Interface\\AddOns\\SkyMythicHistory\\Media\\arrow.tga"
local DEFAULT_DUNGEON_ICON = "Interface\\Icons\\achievement_challengemode_gold"
local DEFAULT_DUNGEON_BG = "Interface\\DialogFrame\\UI-DialogBox-Background-Dark"
local DUNGEON_BACKGROUND_BASE = "Interface\\AddOns\\SkyMythicHistory\\Media\\DungeonBackgrounds\\"
local DEFAULT_CUSTOM_DUNGEON_BG = DUNGEON_BACKGROUND_BASE .. "placeholder.tga"
local PLAYER_NOTE_ICON = "Interface\\AddOns\\SkyMythicHistory\\Media\\note.tga"
local MAX_LOOT_ICONS = 5
local RUN_ROWS = 6
local PLAYER_ROWS = 6
local RECENT_RUN_ROWS = 4
local COMPLETION_ROWS = 5
local PLAYER_DETAIL_HISTORY_ROWS = 5
local PLAYER_DETAIL_HISTORY_MAX_ROWS = 8
local PLAYER_DETAIL_SPEC_CHIPS = 6
local RUN_ROW_HEIGHT = 72
local PLAYER_ROW_HEIGHT = 60
local COMPLETION_ROW_HEIGHT = 52
local PLAYER_TABLE_ROW_HEIGHT = 18
local PLAYER_DETAIL_HISTORY_ROW_HEIGHT = 52
local PLAYER_DETAIL_HERO_HEIGHT = 304
local PLAYER_DETAIL_NOTE_HEIGHT = 76
local PLAYER_DETAIL_HISTORY_HEIGHT = 326
local PLAYER_DETAIL_CONTENT_HEIGHT = PLAYER_DETAIL_HERO_HEIGHT + PLAYER_DETAIL_NOTE_HEIGHT + PLAYER_DETAIL_HISTORY_HEIGHT + 48
local COMPLETION_POPUP_HEIGHT = 492
local MINIMAP_BUTTON_SIZE = 32
local MINIMAP_BUTTON_ICON_INSET = 7
local MINIMAP_BUTTON_EDGE_OVERLAP = 8
local COMPACT_STATS_PLAYER_WIDTH = 170
local COMPACT_STATS_ROW_INSET_X = 12
local COMPACT_STATS_SCORE_X = 232
local COMPACT_STATS_ITEMS_X = 326
local COMPACT_STATS_DAMAGE_X = 520
local COMPACT_STATS_DPS_X = 600
local COMPACT_STATS_HEALING_X = 672
local COMPACT_STATS_HPS_X = 750
local COMPACT_STATS_INTERRUPTS_X = 824
local COMPACT_STATS_DEATHS_X = 922
local COMPACT_STATS_LOOT_ICON_SIZE = 26
local COMPACT_STATS_LOOT_SPACING = 4
local DUNGEON_BACKGROUND_ASPECT = 2
local DUNGEON_HEADER_BACKGROUND_ASPECT = 1050 / 110
local ADDON_TITLE_TEXT = "Sky Mythic History"

local MAX_STATS_DUNGEON_ROWS = 8
local MAX_STATS_CLASS_ROWS = 13
local MAX_STATS_TEAMMATE_ROWS = 10
local STATS_BAR_H = 8
local STATS_DUNGEON_ROW_H = 30
local STATS_CLASS_ROW_H = 28
local STATS_TEAMMATE_ROW_H = 34
local STATS_SECTION_TITLE_H = 40
local STATS_PAD = 12
local STATS_OVERVIEW_H = 80
local STATS_DUNGEON_SECTION_H = STATS_SECTION_TITLE_H + MAX_STATS_DUNGEON_ROWS * STATS_DUNGEON_ROW_H + STATS_PAD
local STATS_CLASS_SECTION_H = STATS_SECTION_TITLE_H + MAX_STATS_CLASS_ROWS * STATS_CLASS_ROW_H + STATS_PAD
local STATS_TWOCOL_H = (STATS_CLASS_SECTION_H > STATS_DUNGEON_SECTION_H) and STATS_CLASS_SECTION_H or STATS_DUNGEON_SECTION_H
local STATS_TEAMMATE_SECTION_H = STATS_SECTION_TITLE_H + MAX_STATS_TEAMMATE_ROWS * STATS_TEAMMATE_ROW_H + STATS_PAD
local STATS_CONTENT_H = STATS_PAD + STATS_OVERVIEW_H + STATS_PAD + STATS_DUNGEON_SECTION_H + STATS_PAD + STATS_TWOCOL_H + STATS_PAD + STATS_TEAMMATE_SECTION_H + STATS_PAD

local cos = math.cos
local sin = math.sin
local rad = math.rad
local deg = math.deg
local atan2 = math.atan2 or math.atan
local floor = math.floor
local min = math.min
local max = math.max
local AceGUI = LibStub and LibStub("AceGUI-3.0", true)
local ScrollingTable = LibStub and LibStub("ScrollingTable", true)
local ROLE_ICON_TEXTURE = "Interface\\LFGFrame\\UI-LFG-ICON-ROLES"

local function GetMinimapButtonRadius()
    local width = Minimap and Minimap:GetWidth() or 140
    local height = Minimap and Minimap:GetHeight() or width
    return (min(width or 140, height or 140) / 2) + (MINIMAP_BUTTON_SIZE / 2) - MINIMAP_BUTTON_EDGE_OVERLAP
end

local COLORS = {
    frameBG = {0.07, 0.07, 0.07, 0.94},
    frameBorder = {0, 0, 0, 1},
    headerBG = {0.09, 0.09, 0.09, 1},
    panelBG = {0.08, 0.08, 0.08, 0.92},
    sectionBG = {0.09, 0.09, 0.09, 1},
    rowBG = {0.095, 0.095, 0.095, 1},
    rowSelected = {0.22, 0.235, 0.225, 1},
    surface = {0, 0, 0, 1},
    accent = {0.7, 0.7, 0.7, 1},
    accentSoft = {0.7, 0.7, 0.7, 0.5},
    success = {0.35, 0.85, 0.35, 1},
    bestMetric = {1.0, 0.82, 0.28, 1},
    danger = {0.85, 0.25, 0.25, 1},
    warning = {0.85, 0.75, 0.20, 1},
    text = {1, 1, 1, 1},
    muted = {0.7, 0.7, 0.7, 1},
    subdued = {0.5, 0.5, 0.5, 1},
    bright = {1, 1, 1, 1},
    shadow = {0, 0, 0, 0},
}

-- Dynamic class color accent (like Cell's accent system)
do
    local _class = select(2, UnitClass("player"))
    if _class and RAID_CLASS_COLORS and RAID_CLASS_COLORS[_class] then
        local cc = RAID_CLASS_COLORS[_class]
        COLORS.accent = {cc.r, cc.g, cc.b, 1}
        COLORS.accentSoft = {cc.r, cc.g, cc.b, 0.5}
    end
end

local STATUS_OPTIONS = {
    {value = "all", label = "All"},
    {value = "timed", label = "Timed"},
    {value = "overtime", label = "Overtime"},
    {value = "abandoned", label = "Abandoned"},
}

local OWNED_CHARACTER_FILTER_OPTIONS = {
    {value = "all", label = "All runs"},
    {value = "mine", label = "Only my characters"},
}

local POPUP_TRIGGER_OPTIONS = {
    {value = "LOOT_OPENED", label = "After opening the chest"},
    {value = "COMPLETED", label = "When the run ends"},
}

local SEASON_OPTIONS = {
    {value = "season1", label = "Season 1"},
}

local SEASON_DUNGEONS = {
    season1 = {
        {value = "all", label = "All dungeons"},
        {value = "Magisters' Terrace", label = "Magisters' Terrace"},
        {value = "Maisara Caverns", label = "Maisara Caverns"},
        {value = "Nexus-Point Xenas", label = "Nexus-Point Xenas"},
        {value = "Windrunner Spire", label = "Windrunner Spire"},
        {value = "Algeth'ar Academy", label = "Algeth'ar Academy"},
        {value = "Pit of Saron", label = "Pit of Saron"},
        {value = "Seat of the Triumvirate", label = "Seat of the Triumvirate"},
        {value = "Skyreach", label = "Skyreach"},
    },
}

local DUNGEON_BACKGROUNDS = {
    ["Magisters' Terrace"] = DUNGEON_BACKGROUND_BASE .. "magisters_terrace.tga",
    ["Maisara Caverns"] = DUNGEON_BACKGROUND_BASE .. "maisara_caverns.tga",
    ["Nexus-Point Xenas"] = DUNGEON_BACKGROUND_BASE .. "nexus_point_xenas.tga",
    ["Windrunner Spire"] = DUNGEON_BACKGROUND_BASE .. "windrunner_spire.tga",
    ["Algeth'ar Academy"] = DUNGEON_BACKGROUND_BASE .. "algethar_academy.tga",
    ["Pit of Saron"] = DUNGEON_BACKGROUND_BASE .. "pit_of_saron.tga",
    ["Seat of the Triumvirate"] = DUNGEON_BACKGROUND_BASE .. "seat_of_the_triumvirate.tga",
    ["Skyreach"] = DUNGEON_BACKGROUND_BASE .. "skyreach.tga",
}

local function SetTextColor(fontString, color)
    fontString:SetTextColor(color[1], color[2], color[3], color[4] or 1)
end

local function ColorizeInlineText(text, color)
    if type(text) ~= "string" or text == "" or type(color) ~= "table" then
        return text or ""
    end

    local r = min(255, max(0, floor((color[1] or 1) * 255 + 0.5)))
    local g = min(255, max(0, floor((color[2] or 1) * 255 + 0.5)))
    local b = min(255, max(0, floor((color[3] or 1) * 255 + 0.5)))
    return ("|cff%02x%02x%02x%s|r"):format(r, g, b, text)
end

local function CreateFont(parent, size, color, justifyH, layer, outline)
    local fontString = parent:CreateFontString(nil, layer or "OVERLAY")
    fontString:SetFont(STANDARD_TEXT_FONT, size, outline or "")
    fontString:SetJustifyH(justifyH or "LEFT")
    fontString:SetJustifyV("MIDDLE")
    fontString:SetShadowColor(0, 0, 0)
    fontString:SetShadowOffset(1, -1)
    SetTextColor(fontString, color or COLORS.text)
    return fontString
end

local function GetStatusLabel(status)
    for _, info in ipairs(STATUS_OPTIONS) do
        if info.value == status then
            return info.label
        end
    end
    return STATUS_OPTIONS[1].label
end

local function GetPopupTriggerLabel(trigger)
    for _, info in ipairs(POPUP_TRIGGER_OPTIONS) do
        if info.value == trigger then
            return info.label
        end
    end
    return POPUP_TRIGGER_OPTIONS[1].label
end

local function GetPopupTriggerNext(trigger)
    for index, info in ipairs(POPUP_TRIGGER_OPTIONS) do
        if info.value == trigger then
            local nextInfo = POPUP_TRIGGER_OPTIONS[index + 1] or POPUP_TRIGGER_OPTIONS[1]
            return nextInfo.value
        end
    end
    return POPUP_TRIGGER_OPTIONS[1].value
end

local function GetSeasonLabel(season)
    for _, info in ipairs(SEASON_OPTIONS) do
        if info.value == season then
            return info.label
        end
    end
    return SEASON_OPTIONS[1].label
end

local function GetSeasonNext(season)
    for index, info in ipairs(SEASON_OPTIONS) do
        if info.value == season then
            local nextInfo = SEASON_OPTIONS[index + 1] or SEASON_OPTIONS[1]
            return nextInfo.value
        end
    end
    return SEASON_OPTIONS[1].value
end

local function GetDungeonOptionsForSeason(season)
    return SEASON_DUNGEONS[season] or SEASON_DUNGEONS[SEASON_OPTIONS[1].value]
end

local function GetOwnedCharacterFilterOptions()
    local options = {
        {value = "all", label = "All runs"},
        {value = "mine", label = "Any account character"},
    }

    local characters = MythicTools.GetOwnedCharacters and MythicTools:GetOwnedCharacters() or {}
    local shortNameCounts = {}
    for _, fullName in ipairs(characters) do
        local shortName = MythicTools:GetShortName(fullName)
        shortNameCounts[shortName] = (shortNameCounts[shortName] or 0) + 1
    end

    for _, fullName in ipairs(characters) do
        local shortName = MythicTools:GetShortName(fullName)
        options[#options + 1] = {
            value = fullName,
            label = (shortNameCounts[shortName] or 0) > 1 and fullName or shortName,
        }
    end

    return options
end

local function NormalizeDateFilter(text)
    text = MythicTools:TrimText(text)
    if text == "" then
        return ""
    end

    local day, month, year = text:match("^(%d%d?)/(%d%d?)/(%d%d%d%d)$")
    if day and month and year then
        return ("%02d/%02d/%04d"):format(tonumber(day), tonumber(month), tonumber(year))
    end

    year, month, day = text:match("^(%d%d%d%d)%-(%d%d?)%-(%d%d?)$")
    if year and month and day then
        return ("%02d/%02d/%04d"):format(tonumber(day), tonumber(month), tonumber(year))
    end

    return MythicTools:Lower(text)
end

local function GetRunStatusColor(run)
    local result = MythicTools:GetRunResult(run)
    if result == "timed" then
        return COLORS.success
    end
    if result == "abandoned" then
        return COLORS.danger
    end
    return COLORS.warning
end

local function GetRunStatusText(run)
    local result = MythicTools:GetRunResult(run)
    if result == "timed" then
        local upgrades = min(3, max(0, tonumber(run and run.keystoneUpgradeLevels) or 0))
        return upgrades > 0 and ("+%d"):format(upgrades) or "Timed"
    end
    if result == "abandoned" then
        return "Abandoned"
    end
    return "Overtime"
end

local function GetRunOwnerInfo(run)
    if type(run) ~= "table" then
        return nil, nil
    end

    local owner = MythicTools:NormalizePlayerName(run.ownerCharacter) or MythicTools:TrimText(run.ownerCharacter)
    if not owner or owner == "" then
        owner = run.roster and run.roster[1] or nil
    end

    return owner, owner and run.playerStats and run.playerStats[owner] or nil
end

local function GetDungeonIcon(run)
    return run and (run.textureFileID or run.backgroundTextureFileID) or DEFAULT_DUNGEON_ICON
end

local function GetDungeonBackground(run)
    local dungeonName = run and run.dungeonName
    if dungeonName and DUNGEON_BACKGROUNDS[dungeonName] then
        return DUNGEON_BACKGROUNDS[dungeonName]
    end

    return DEFAULT_CUSTOM_DUNGEON_BG
end

local function NormalizeDisplayItemLink(itemLink)
    if type(itemLink) ~= "string" or itemLink == "" then
        return nil
    end

    return itemLink:match("(|c%x+|Hitem:.-|h%[.-%]|h|r)")
        or itemLink:match("(|Hitem:.-|h%[.-%]|h)")
        or itemLink
end

local function GetItemIDFromLinkish(itemLink)
    if type(itemLink) ~= "string" or itemLink == "" then
        return nil
    end

    local itemID = itemLink:match("Hitem:(%d+)")
        or itemLink:match("|Hitem:(%d+)")
        or itemLink:match("^item:(%d+)")
        or itemLink:match("item:(%d+)")

    return itemID and tonumber(itemID) or nil
end

local function ExtractItemNameFromLink(itemLink)
    if type(itemLink) ~= "string" then
        return nil
    end

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

    return nil
end

local function GetItemNameFallback(itemLink)
    itemLink = NormalizeDisplayItemLink(itemLink)
    if not itemLink then
        return ""
    end

    local itemName = GetItemInfo(itemLink)
    if itemName and itemName ~= "" then
        return itemName
    end

    itemName = ExtractItemNameFromLink(itemLink)
    if itemName and itemName ~= "" then
        return itemName
    end

    local itemID = GetItemIDFromLinkish(itemLink)
    if itemID and C_Item and C_Item.GetItemNameByID then
        itemName = C_Item.GetItemNameByID(itemID)
        if itemName and itemName ~= "" then
            return itemName
        end
    end

    return "Unknown item"
end

local function GetItemQualityColorByLink(itemLink)
    itemLink = NormalizeDisplayItemLink(itemLink)
    if not itemLink or not C_Item or not C_Item.GetItemQualityColor then
        return nil
    end

    local itemName, itemLinkResolved, itemQuality = GetItemInfo(itemLink)
    if itemName and itemQuality then
        local r, g, b = C_Item.GetItemQualityColor(itemQuality)
        if r and g and b then
            return {r, g, b, 1}
        end
    end

    local itemID = GetItemIDFromLinkish(itemLink)
    if itemID and C_Item and C_Item.GetItemQualityByID then
        local quality = C_Item.GetItemQualityByID(itemID)
        if quality then
            local r, g, b = C_Item.GetItemQualityColor(quality)
            if r and g and b then
                return {r, g, b, 1}
            end
        end
    end

    return nil
end

local function GetItemLevelForLink(itemLink)
    itemLink = NormalizeDisplayItemLink(itemLink)
    if not itemLink then
        return nil
    end

    local itemLevel = GetDetailedItemLevelInfo and GetDetailedItemLevelInfo(itemLink) or nil
    itemLevel = tonumber(itemLevel) or itemLevel
    if itemLevel and itemLevel > 0 then
        return itemLevel
    end

    if C_Item and C_Item.GetDetailedItemLevelInfo then
        local detailedItemLevel = C_Item.GetDetailedItemLevelInfo(itemLink)
        itemLevel = tonumber(detailedItemLevel) or nil
        if itemLevel and itemLevel > 0 then
            return itemLevel
        end
    end

    return nil
end

local function FormatItemLevelText(itemLevel)
    itemLevel = tonumber(itemLevel)
    if not itemLevel or itemLevel <= 0 then
        return ""
    end

    if math.abs(itemLevel - math.floor(itemLevel + 0.5)) < 0.05 then
        return tostring(math.floor(itemLevel + 0.5))
    end

    return ("%.1f"):format(itemLevel)
end

local function GetClassIconMarkup(classFilename, specIconID, specID)
    if specID or (specIconID and not classFilename) then
        local icon = MythicTools.ResolveDisplayIcon and MythicTools:ResolveDisplayIcon(specIconID, specID) or specIconID
        return ("|T%s:16:16:0:0|t"):format(tostring(icon or QUESTION_MARK_ICON))
    end

    local coords = classFilename and CLASS_ICON_TCOORDS and CLASS_ICON_TCOORDS[classFilename]
    if coords then
        return ("|T%s:16:16:0:0:256:256:%d:%d:%d:%d|t"):format(
            CLASS_CIRCLE_TEXTURE,
            math.floor(coords[1] * 256),
            math.floor(coords[2] * 256),
            math.floor(coords[3] * 256),
            math.floor(coords[4] * 256)
        )
    end

    return ("|T%s:16:16:0:0|t"):format(QUESTION_MARK_ICON)
end

function MythicTools:ApplySurface(frame, bgColor, borderColor, accentColor)
    local surface = frame._mtSurface
    if not surface then
        local bg = frame:CreateTexture(nil, "BACKGROUND")
        bg:SetAllPoints()
        bg:SetTexture(WHITE_TEXTURE)

        local top = frame:CreateTexture(nil, "ARTWORK")
        top:SetTexture(WHITE_TEXTURE)
        top:SetPoint("TOPLEFT")
        top:SetPoint("TOPRIGHT")
        top:SetHeight(1)

        local bottom = frame:CreateTexture(nil, "ARTWORK")
        bottom:SetTexture(WHITE_TEXTURE)
        bottom:SetPoint("BOTTOMLEFT")
        bottom:SetPoint("BOTTOMRIGHT")
        bottom:SetHeight(1)

        local left = frame:CreateTexture(nil, "ARTWORK")
        left:SetTexture(WHITE_TEXTURE)
        left:SetPoint("TOPLEFT")
        left:SetPoint("BOTTOMLEFT")
        left:SetWidth(1)

        local right = frame:CreateTexture(nil, "ARTWORK")
        right:SetTexture(WHITE_TEXTURE)
        right:SetPoint("TOPRIGHT")
        right:SetPoint("BOTTOMRIGHT")
        right:SetWidth(1)

        surface = {
            bg = bg,
            top = top,
            bottom = bottom,
            left = left,
            right = right,
        }
        frame._mtSurface = surface
    end

    surface.bg:SetColorTexture(bgColor[1], bgColor[2], bgColor[3], bgColor[4] or 1)
    surface.top:SetColorTexture(borderColor[1], borderColor[2], borderColor[3], borderColor[4] or 1)
    surface.bottom:SetColorTexture(borderColor[1], borderColor[2], borderColor[3], borderColor[4] or 1)
    surface.left:SetColorTexture(borderColor[1], borderColor[2], borderColor[3], borderColor[4] or 1)
    surface.right:SetColorTexture(borderColor[1], borderColor[2], borderColor[3], borderColor[4] or 1)
end

local function StyleEditBox(editBox)
    editBox:SetAutoFocus(false)
    editBox:SetTextInsets(10, 10, 0, 0)
    editBox:SetFont(STANDARD_TEXT_FONT, 13, "")
    editBox:SetJustifyH("LEFT")
    if editBox.Left then editBox.Left:SetAlpha(0) end
    if editBox.Middle then editBox.Middle:SetAlpha(0) end
    if editBox.Right then editBox.Right:SetAlpha(0) end
end

local function ClipChildren(frame)
    if frame and frame.SetClipsChildren then
        frame:SetClipsChildren(true)
    end
end

local function SetTextureCoverTexCoord(texture, sourceAspect)
    if not texture then
        return
    end

    local width = texture:GetWidth() or 0
    local height = texture:GetHeight() or 0
    local targetAspect = height > 0 and (width / height) or 0
    sourceAspect = tonumber(sourceAspect) or DUNGEON_BACKGROUND_ASPECT
    if targetAspect <= 0 or sourceAspect <= 0 then
        texture:SetTexCoord(0, 1, 0, 1)
        return
    end

    if targetAspect > sourceAspect then
        local visibleHeight = max(0.05, min(1, sourceAspect / targetAspect))
        local top = (1 - visibleHeight) / 2
        texture:SetTexCoord(0, 1, top, top + visibleHeight)
    else
        local visibleWidth = max(0.05, min(1, targetAspect / sourceAspect))
        local left = (1 - visibleWidth) / 2
        texture:SetTexCoord(left, left + visibleWidth, 0, 1)
    end
end

local function CreateLabeledEditBox(parent, labelText, width, onChanged)
    local holder = CreateFrame("Frame", nil, parent)
    holder:SetSize(width, 50)

    holder.Label = CreateFont(holder, 11, COLORS.subdued)
    holder.Label:SetPoint("TOPLEFT", 2, 0)
    holder.Label:SetText(labelText)

    holder.Bg = CreateFrame("Frame", nil, holder, "BackdropTemplate")
    holder.Bg:SetPoint("TOPLEFT", 0, -18)
    holder.Bg:SetSize(width, 28)
    holder.Bg:SetBackdrop({bgFile = WHITE_TEXTURE, edgeFile = WHITE_TEXTURE, edgeSize = 1, insets = {left=1, right=1, top=1, bottom=1}})
    holder.Bg:SetBackdropColor(COLORS.frameBG[1], COLORS.frameBG[2], COLORS.frameBG[3], 1)
    holder.Bg:SetBackdropBorderColor(0, 0, 0, 1)

    holder.Input = CreateFrame("EditBox", nil, holder.Bg, "InputBoxTemplate")
    holder.Input:SetPoint("TOPLEFT", 0, 0)
    holder.Input:SetPoint("BOTTOMRIGHT", 0, 0)
    StyleEditBox(holder.Input)
    holder.Input:SetScript("OnEscapePressed", function(editBox) editBox:ClearFocus() end)
    holder.Input:SetScript("OnEnterPressed", function(editBox)
        editBox:ClearFocus()
        if onChanged then
            onChanged(editBox:GetText())
        end
    end)
    holder.Input:SetScript("OnTextChanged", function(editBox, userInput)
        if userInput and onChanged then
            onChanged(editBox:GetText())
        end
    end)

    function holder:SetValue(value)
        if not self.Input:HasFocus() then
            self.Input:SetText(value or "")
        end
    end

    return holder
end

local function CreateActionButton(parent, width, height, label)
    local button = CreateFrame("Button", nil, parent, "BackdropTemplate")
    button:SetSize(width, height)
    button:SetBackdrop({bgFile = WHITE_TEXTURE, edgeFile = WHITE_TEXTURE, edgeSize = 1, insets = {left=1, right=1, top=1, bottom=1}})
    button:SetBackdropColor(COLORS.rowBG[1], COLORS.rowBG[2], COLORS.rowBG[3], 1)
    button:SetBackdropBorderColor(0, 0, 0, 1)

    button.Text = CreateFont(button, 12, COLORS.text, "CENTER", "OVERLAY", "")
    button.Text:SetPoint("CENTER")
    button.Text:SetText(label)

    button._restyle = function(self)
        if self._selected then
            button:SetBackdropColor(COLORS.accent[1], COLORS.accent[2], COLORS.accent[3], 0.3)
            button:SetBackdropBorderColor(COLORS.accent[1], COLORS.accent[2], COLORS.accent[3], 1)
            SetTextColor(self.Text, COLORS.bright)
        else
            button:SetBackdropColor(COLORS.rowBG[1], COLORS.rowBG[2], COLORS.rowBG[3], 1)
            button:SetBackdropBorderColor(0, 0, 0, 1)
            SetTextColor(self.Text, COLORS.text)
        end
    end

    button:SetScript("OnEnter", function(self)
        if not self._selected then
            button:SetBackdropColor(COLORS.rowSelected[1], COLORS.rowSelected[2], COLORS.rowSelected[3], 1)
            SetTextColor(self.Text, COLORS.bright)
        end
    end)
    button:SetScript("OnLeave", function(self)
        if self._restyle then
            self:_restyle()
        end
    end)

    button:_restyle()
    return button
end

local function CreateCycleControl(parent, labelText, width, getLabel, onClick)
    local holder = CreateFrame("Frame", nil, parent)
    holder:SetSize(width, 50)

    holder.Label = CreateFont(holder, 11, COLORS.subdued)
    holder.Label:SetPoint("TOPLEFT", 2, 0)
    holder.Label:SetText(labelText)

    holder.Button = CreateActionButton(holder, width, 28, "")
    holder.Button:SetPoint("TOPLEFT", 0, -18)
    holder.Button:SetScript("OnClick", function()
        if onClick then
            onClick()
        end
    end)

    function holder:SetValue(value)
        holder.Button.Text:SetText(getLabel and getLabel(value) or tostring(value or ""))
    end

    return holder
end

local function CreateDropdownControl(parent, labelText, width, getOptions, onChanged)
    local holder = CreateFrame("Frame", nil, parent)
    holder:SetSize(width, 50)

    holder.Label = CreateFont(holder, 11, COLORS.subdued)
    holder.Label:SetPoint("TOPLEFT", 2, 0)
    holder.Label:SetText(labelText)

    holder.Button = CreateFrame("Button", nil, holder, "BackdropTemplate")
    holder.Button:SetPoint("TOPLEFT", 0, -18)
    holder.Button:SetSize(width, 28)
    holder.Button:SetBackdrop({bgFile = WHITE_TEXTURE, edgeFile = WHITE_TEXTURE, edgeSize = 1, insets = {left=1, right=1, top=1, bottom=1}})
    holder.Button:SetBackdropColor(COLORS.rowBG[1], COLORS.rowBG[2], COLORS.rowBG[3], 1)
    holder.Button:SetBackdropBorderColor(0, 0, 0, 1)
    holder.Button:SetScript("OnEnter", function() holder.Button:SetBackdropColor(COLORS.rowSelected[1], COLORS.rowSelected[2], COLORS.rowSelected[3], 1) end)
    holder.Button:SetScript("OnLeave", function() holder.Button:SetBackdropColor(COLORS.rowBG[1], COLORS.rowBG[2], COLORS.rowBG[3], 1) end)

    holder.Button.Text = CreateFont(holder.Button, 12, COLORS.text)
    holder.Button.Text:SetPoint("LEFT", 10, 0)
    holder.Button.Text:SetPoint("RIGHT", -24, 0)
    holder.Button.Text:SetJustifyH("LEFT")

    holder.Button.Arrow = CreateFont(holder.Button, 11, COLORS.muted, "RIGHT")
    holder.Button.Arrow:SetPoint("RIGHT", -9, 0)
    holder.Button.Arrow:SetText("v")

    holder.Button:SetScript("OnClick", function(button)
        local menu = {}
        for _, info in ipairs(getOptions and getOptions() or {}) do
            menu[#menu + 1] = {
                text = info.label,
                checked = holder.value == info.value,
                notCheckable = false,
                func = function()
                    holder:SetValue(info.value)
                    if onChanged then
                        onChanged(info.value)
                    end
                end,
            }
        end

        MythicTools.dropdownMenuFrame = MythicTools.dropdownMenuFrame or CreateFrame("Frame", "MythicToolsDropdownMenu", UIParent, "UIDropDownMenuTemplate")
        MythicTools.dropdownMenuFrame:SetFrameStrata("FULLSCREEN_DIALOG")
        EasyMenu(menu, MythicTools.dropdownMenuFrame, "cursor", 0, 0, "MENU")
    end)

    function holder:SetValue(value)
        holder.value = value
        local selectedLabel = tostring(value or "")
        for _, info in ipairs(getOptions and getOptions() or {}) do
            if info.value == value then
                selectedLabel = info.label
                break
            end
        end
        holder.Button.Text:SetText(selectedLabel)
    end

    return holder
end

local function AttachAceContainer(widget, parent, left, top, right, height)
    widget.frame:SetParent(parent)
    widget.frame:ClearAllPoints()
    widget.frame:SetPoint("TOPLEFT", parent, "TOPLEFT", left, top)
    widget.frame:SetPoint("TOPRIGHT", parent, "TOPRIGHT", right, top)
    widget.frame:SetHeight(height)
    widget.frame:Show()
    return widget
end

local function CreateAceFlowRow(parent, topOffset, height)
    if not AceGUI then
        return nil
    end

    local group = AceGUI:Create("SimpleGroup")
    group:SetLayout("Flow")
    return AttachAceContainer(group, parent, 12, topOffset, -12, height)
end

local function CreateAceSpacer(width)
    if not AceGUI then return nil end
    local spacer = AceGUI:Create("Label")
    spacer:SetWidth(width)
    spacer:SetText("")
    return spacer
end

local function StyleAceEditWidget(widget)
    if not widget then return end
    local eb = widget.editBox
    if eb then
        if eb.SetBackdropColor then
            eb:SetBackdropColor(0.20, 0.20, 0.20, 1)
        end
        if eb.SetBackdropBorderColor then
            eb:SetBackdropBorderColor(0.35, 0.35, 0.35, 0.9)
        end
    end
end

local function StyleAceDropdownWidget(widget)
    if not widget then return end
    local btn = widget.button
    if btn then
        if btn.SetBackdropColor then
            btn:SetBackdropColor(0.20, 0.20, 0.20, 1)
        end
        if btn.SetBackdropBorderColor then
            btn:SetBackdropBorderColor(0.35, 0.35, 0.35, 0.9)
        end
    end
end

local function ApplyThinScrollBar(scrollFrame)
    if not scrollFrame then return end
    local scrollBar
    for _, child in ipairs({scrollFrame:GetChildren()}) do
        if child.GetThumbTexture or child:GetObjectType() == "Slider" then
            scrollBar = child
            break
        end
    end
    if not scrollBar then return end

    scrollBar:SetWidth(6)
    scrollBar:ClearAllPoints()
    scrollBar:SetPoint("TOPRIGHT", scrollFrame, "TOPRIGHT", 0, -1)
    scrollBar:SetPoint("BOTTOMRIGHT", scrollFrame, "BOTTOMRIGHT", 0, 1)

    for _, btn in ipairs({scrollBar:GetChildren()}) do
        btn:SetAlpha(0)
        btn:EnableMouse(false)
    end

    local numRegions = scrollBar.GetNumRegions and scrollBar:GetNumRegions() or 0
    for i = 1, numRegions do
        local region = select(i, scrollBar:GetRegions())
        if region and region.SetTexture then
            region:SetTexture(nil)
        end
    end

    local thumb = scrollBar.GetThumbTexture and scrollBar:GetThumbTexture()
    if thumb then
        thumb:SetTexture(WHITE_TEXTURE)
        thumb:SetWidth(4)
        thumb:SetVertexColor(COLORS.accent[1], COLORS.accent[2], COLORS.accent[3], 0.65)
    end
end

local function ApplyThinScrollBarToST(tableWidget)
    if not tableWidget or not tableWidget.scrollframe then return end
    local scrollframe = tableWidget.scrollframe
    local f = tableWidget.frame

    -- Hide lib-st's custom scroll trough frames (they are Frame children of scrollframe)
    for _, child in ipairs({scrollframe:GetChildren()}) do
        if child:GetObjectType() == "Frame" then
            child:Hide()
        end
    end

    -- Expand the scrollframe into the space freed by removing the wide trough
    scrollframe:ClearAllPoints()
    scrollframe:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -4)
    scrollframe:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -10, 3)

    ApplyThinScrollBar(scrollframe)
end

local function CreateAceEditWidget(labelText, width, value, onChanged)
    if not AceGUI then
        return nil
    end

    local widget = AceGUI:Create("EditBox")
    widget:SetLabel(labelText)
    widget:SetWidth(width)
    widget:DisableButton(true)
    widget:SetText(value or "")
    widget:SetCallback("OnEnterPressed", function(_, _, text)
        if widget._mtSettingValue then
            return
        end
        if onChanged then
            onChanged(text or "")
        end
    end)
    widget:SetCallback("OnTextChanged", function(_, _, text)
        if widget._mtSettingValue then
            return
        end
        if onChanged then
            onChanged(text or "")
        end
    end)
    return widget
end

local function CreateAceDropdownWidget(labelText, width, options, value, onChanged)
    if not AceGUI then
        return nil
    end

    local list = {}
    local order = {}
    for _, info in ipairs(options or {}) do
        list[info.value] = info.label
        order[#order + 1] = info.value
    end

    local widget = AceGUI:Create("Dropdown")
    widget:SetLabel(labelText)
    widget:SetWidth(width)
    widget:SetList(list, order)
    widget:SetValue(value)
    widget:SetCallback("OnValueChanged", function(_, _, selectedValue)
        if widget._mtSettingValue then
            return
        end
        if onChanged then
            onChanged(selectedValue)
        end
    end)
    return widget
end

local function UpdateAceDropdownOptions(widget, options, value)
    if not widget then
        return
    end

    local list = {}
    local order = {}
    for _, info in ipairs(options or {}) do
        list[info.value] = info.label
        order[#order + 1] = info.value
    end

    widget._mtSettingValue = true
    widget:SetList(list, order)
    widget:SetValue(value)
    widget._mtSettingValue = nil
end

local function SetAceEditText(widget, value)
    if widget then
        widget._mtSettingValue = true
        widget:SetText(value or "")
        widget._mtSettingValue = nil
    end
end

local function SetAceDropdownValue(widget, value)
    if widget then
        widget._mtSettingValue = true
        widget:SetValue(value)
        widget._mtSettingValue = nil
    end
end

function MythicTools:RefreshOwnedCharacterFilterOptions()
    if not (self.ui and self.ui.runsOwnedFilter) then
        return
    end

    local options = GetOwnedCharacterFilterOptions()
    local selectedValue = (self.db and self.db.ui and self.db.ui.filters and self.db.ui.filters.ownedCharacters) or "all"
    local hasSelectedValue = false

    for _, info in ipairs(options) do
        if info.value == selectedValue then
            hasSelectedValue = true
            break
        end
    end

    if not hasSelectedValue then
        selectedValue = "all"
        self.db.ui.filters.ownedCharacters = selectedValue
    end

    UpdateAceDropdownOptions(self.ui.runsOwnedFilter, options, selectedValue)
end

function MythicTools:ResetRunsFilters()
    self.db.ui.filters = {
        search = "",
        player = "",
        dungeon = "all",
        season = "season1",
        date = "",
        status = "all",
        ownedCharacters = "all",
    }
end

function MythicTools:ResetPlayerFilters()
    self.db.ui.playerSearch = ""
    self.db.ui.playerFilters = {
        search = "",
        season = "season1",
        dungeon = "all",
    }
end

function MythicTools:ResetNavigationFilters()
    self:ResetRunsFilters()
    self:ResetPlayerFilters()
end

function MythicTools:SyncFilterWidgets()
    if not self.ui then
        return
    end

    local runFilters = self.db.ui.filters or {}
    SetAceEditText(self.ui.runsSearch, runFilters.search)
    SetAceEditText(self.ui.runsPlayerFilter, runFilters.player)
    SetAceDropdownValue(self.ui.runsSeasonFilter, runFilters.season)
    UpdateAceDropdownOptions(self.ui.runsDungeonFilter, GetDungeonOptionsForSeason(runFilters.season), runFilters.dungeon)
    SetAceEditText(self.ui.runsDateFilter, runFilters.date)
    SetAceDropdownValue(self.ui.runsStatusFilter, runFilters.status)
    self:RefreshOwnedCharacterFilterOptions()

    local playerFilters = self.db.ui.playerFilters or {}
    SetAceEditText(self.ui.playerSearch, playerFilters.search or self.db.ui.playerSearch)
    SetAceDropdownValue(self.ui.playersSeasonFilter, playerFilters.season or "season1")
    UpdateAceDropdownOptions(self.ui.playersDungeonFilter, GetDungeonOptionsForSeason(playerFilters.season or "season1"), playerFilters.dungeon or "all")
end

function MythicTools:ResolveDisplayIcon(icon, specID, classFilename)
    if specID then
        local _, _, specIconID = self:GetSpecInfo(specID)
        if specIconID then
            return specIconID
        end
    end

    if classFilename then
        return QUESTION_MARK_ICON
    end

    if type(icon) == "number" then
        if icon <= 0 then
            return QUESTION_MARK_ICON
        end
        if C_Spell and C_Spell.GetSpellTexture then
            local spellIcon = C_Spell.GetSpellTexture(icon)
            if spellIcon then
                return spellIcon
            end
        end
        return icon
    end

    if type(icon) == "string" then
        local value = self:TrimText(icon)
        if value == "" then
            return QUESTION_MARK_ICON
        end
        local numericIcon = tonumber(value)
        if numericIcon then
            return self:ResolveDisplayIcon(numericIcon, specID, classFilename)
        end
        if value:match("^[Ii]nterface\\") then
            return value
        end
    end

    return QUESTION_MARK_ICON
end

function MythicTools:ResolveTooltipHistoryPlayerName(playerName)
    if not (self.db and self.db.playersIndex) then
        return nil
    end

    local fullName = self:NormalizePlayerName(playerName) or self:TrimText(playerName)
    if not fullName or fullName == "" then
        return nil
    end
    if self.db.playersIndex[fullName] then
        return fullName
    end

    local shortName = self:GetShortName(fullName)
    if shortName == "" then
        return nil
    end

    for indexedName in pairs(self.db.playersIndex) do
        if self:GetShortName(indexedName) == shortName then
            return indexedName
        end
    end

    return nil
end

function MythicTools:GetPlayerTooltipSummary(playerName)
    local fullName = self:ResolveTooltipHistoryPlayerName(playerName)
    if not fullName or fullName == "" then
        return nil
    end

    local playerEntry = self.db and self.db.playersIndex and self.db.playersIndex[fullName] or nil
    local totalRuns = tonumber(playerEntry and playerEntry.totalRuns) or 0
    local timedRuns = tonumber(playerEntry and playerEntry.timedRuns) or 0

    if totalRuns == 0 then
        for _, run in ipairs(self:GetRunsForPlayer(fullName) or {}) do
            totalRuns = totalRuns + 1
            if self:GetRunResult(run) == "timed" then
                timedRuns = timedRuns + 1
            end
        end
    end

    local noteText = self:GetPlayerTooltipNote(fullName)
    if not noteText or noteText == "" then
        noteText = nil
    end

    return {
        fullName = fullName,
        timedRuns = timedRuns,
        totalRuns = totalRuns,
        noteText = noteText,
    }
end

function MythicTools:GetPlayerTooltipNote(fullName)
    local noteText = self:GetPlayerNote(fullName)
    if noteText ~= "" then
        return noteText
    end

    if not (self.db and self.db.playerNotes) then
        return ""
    end

    local shortName = self:GetShortName(fullName)
    if shortName == "" then
        return ""
    end

    for notePlayer, note in pairs(self.db.playerNotes) do
        if type(note) == "string" and note ~= "" and self:GetShortName(notePlayer) == shortName then
            return note
        end
    end

    return ""
end

function MythicTools:AddPlayerHistoryTooltipLines(tooltip, summary)
    tooltip:AddLine(" ")
    tooltip:AddLine(ADDON_TITLE_TEXT, 1, 0.82, 0)
    tooltip:AddLine(("%d of %d timed"):format(summary.timedRuns, summary.totalRuns), 1, 1, 1)
    if summary.noteText then
        tooltip:AddLine(("Note: %s"):format(summary.noteText), 1, 1, 1, true)
    end
end

function MythicTools:ShowPlayerSummaryTooltip(owner, playerName, anchor)
    local summary = self:GetPlayerTooltipSummary(playerName)
    if not summary then
        return
    end

    GameTooltip:SetOwner(owner, anchor or "ANCHOR_TOPLEFT")
    self:AddPlayerHistoryTooltipLines(GameTooltip, summary)
    GameTooltip:Show()
end

function MythicTools:SetTooltipOwnerSafely(tooltip, owner, anchor, xOffset, yOffset)
    if not (tooltip and owner and tooltip.SetOwner) then
        return false, false, false
    end

    local existingOwner = tooltip.GetOwner and tooltip:GetOwner() or nil
    if existingOwner == owner then
        return false, true, true
    end

    tooltip:SetOwner(owner, anchor or "ANCHOR_RIGHT", xOffset or 0, yOffset or 0)
    return true, existingOwner ~= nil, existingOwner == owner
end

local function CreateListButton(parent, height)
    local button = CreateFrame("Button", nil, parent)
    button:SetHeight(height)
    button:RegisterForClicks("LeftButtonUp", "RightButtonUp")
    MythicTools:ApplySurface(button, COLORS.rowBG, COLORS.surface, nil)

    button.Accent = button:CreateTexture(nil, "ARTWORK")
    button.Accent:SetTexture(WHITE_TEXTURE)
    button.Accent:SetPoint("TOPLEFT")
    button.Accent:SetPoint("BOTTOMLEFT")
    button.Accent:SetWidth(3)
    button.Accent:SetVertexColor(COLORS.accent[1], COLORS.accent[2], COLORS.accent[3], 0.25)

    button.IconFrame = CreateFrame("Frame", nil, button)
    button.IconFrame:SetSize(42, 42)
    button.IconFrame:SetPoint("LEFT", 12, 0)
    MythicTools:ApplySurface(button.IconFrame, COLORS.frameBG, COLORS.surface, nil)

    button.Icon = button.IconFrame:CreateTexture(nil, "ARTWORK")
    button.Icon:SetPoint("TOPLEFT", 1, -1)
    button.Icon:SetPoint("BOTTOMRIGHT", -1, 1)
    button.Icon:SetTexture(DEFAULT_DUNGEON_ICON)
    button.Icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)

    button.Title = CreateFont(button, 13, COLORS.text)
    button.Title:SetPoint("LEFT", button.IconFrame, "RIGHT", 12, 10)
    button.Title:SetPoint("RIGHT", button, "RIGHT", -190, 10)

    button.Subtitle = CreateFont(button, 11, COLORS.muted)
    button.Subtitle:SetPoint("TOPLEFT", button.Title, "BOTTOMLEFT", 0, -6)
    button.Subtitle:SetPoint("TOPRIGHT", -104, -28)
    button.Subtitle:SetJustifyH("LEFT")

    button.OwnerRole = CreateFrame("Frame", nil, button)
    button.OwnerRole:SetSize(16, 16)
    button.OwnerRole:SetPoint("LEFT", button.IconFrame, "RIGHT", 12, -11)
    MythicTools:ApplySurface(button.OwnerRole, COLORS.rowBG, COLORS.surface, nil)
    button.OwnerRole.Icon = button.OwnerRole:CreateTexture(nil, "ARTWORK")
    button.OwnerRole.Icon:SetPoint("TOPLEFT", 2, -2)
    button.OwnerRole.Icon:SetPoint("BOTTOMRIGHT", -2, 2)
    button.OwnerRole.Label = CreateFont(button.OwnerRole, 8, COLORS.text, "CENTER")
    button.OwnerRole.Label:SetAllPoints()
    button.OwnerRole:Hide()

    button.OwnerSpec = CreateFrame("Frame", nil, button)
    button.OwnerSpec:SetSize(16, 16)
    button.OwnerSpec:SetPoint("LEFT", button.OwnerRole, "RIGHT", 5, 0)
    MythicTools:ApplySurface(button.OwnerSpec, COLORS.frameBG, COLORS.surface, nil)
    button.OwnerSpec.Icon = button.OwnerSpec:CreateTexture(nil, "ARTWORK")
    button.OwnerSpec.Icon:SetPoint("TOPLEFT", 1, -1)
    button.OwnerSpec.Icon:SetPoint("BOTTOMRIGHT", -1, 1)
    button.OwnerSpec.Icon:SetTexture(QUESTION_MARK_ICON)
    button.OwnerSpec:Hide()

    button.OwnerName = CreateFont(button, 10, COLORS.muted)
    button.OwnerName:SetPoint("LEFT", button.OwnerSpec, "RIGHT", 5, 0)
    button.OwnerName:SetPoint("RIGHT", button, "RIGHT", -190, 0)
    button.OwnerName:SetJustifyH("LEFT")
    button.OwnerName:Hide()

    button.RightTop = CreateFont(button, 11, COLORS.accent, "RIGHT")
    button.RightTop:SetPoint("RIGHT", button, "RIGHT", -12, 10)
    button.RightTop:SetWidth(170)

    button.RightBottom = CreateFont(button, 11, COLORS.subdued, "RIGHT")
    button.RightBottom:SetPoint("RIGHT", button, "RIGHT", -12, -11)
    button.RightBottom:SetWidth(170)

    button:SetHighlightTexture(WHITE_TEXTURE)
    local highlight = button:GetHighlightTexture()
    highlight:SetAllPoints()
    highlight:SetVertexColor(1, 1, 1, 0.04)

    return button
end

function MythicTools:ConfirmDeleteRun(runId)
    if not runId then
        return
    end

    local run = self:GetRunById(runId)
    if not run then
        return
    end

    local label = ("%s +%d"):format(run.dungeonName or "Unknown", tonumber(run.level) or 0)
    StaticPopup_Show("MYTHICTOOLS_DELETE_RUN", label, nil, runId)
end

local function CreateMetricCard(parent, labelText)
    local card = CreateFrame("Frame", nil, parent)
    MythicTools:ApplySurface(card, COLORS.rowBG, COLORS.surface, nil)

    card.Label = CreateFont(card, 10, COLORS.subdued)
    card.Label:SetPoint("TOPLEFT", 12, -10)
    card.Label:SetPoint("TOPRIGHT", -12, -10)
    card.Label:SetJustifyH("LEFT")
    card.Label:SetText(labelText)

    card.Value = CreateFont(card, 18, COLORS.text)
    card.Value:SetPoint("TOPLEFT", card.Label, "BOTTOMLEFT", 0, -8)
    card.Value:SetPoint("BOTTOMLEFT", 12, 10)
    card.Value:SetWidth(96)
    card.Value:SetJustifyH("LEFT")

    card.Meta = CreateFont(card, 10, COLORS.subdued, "RIGHT")
    card.Meta:SetPoint("LEFT", card.Value, "RIGHT", 10, 0)
    card.Meta:SetPoint("RIGHT", card, "RIGHT", -12, 0)
    card.Meta:SetJustifyH("RIGHT")

    return card
end

local function CreateBreakdownRow(parent)
    local row = CreateFrame("Frame", nil, parent)
    row:SetHeight(34)
    MythicTools:ApplySurface(row, COLORS.rowBG, COLORS.surface, nil)

    row.Title = CreateFont(row, 11, COLORS.text)
    row.Title:SetPoint("TOPLEFT", 10, -6)
    row.Title:SetPoint("TOPRIGHT", -10, -6)
    row.Title:SetJustifyH("LEFT")

    row.Meta = CreateFont(row, 10, COLORS.muted)
    row.Meta:SetPoint("TOPLEFT", row.Title, "BOTTOMLEFT", 0, -4)
    row.Meta:SetPoint("TOPRIGHT", -10, -18)
    row.Meta:SetJustifyH("LEFT")

    return row
end

local function CreateSpecChip(parent)
    local chip = CreateFrame("Frame", nil, parent)
    chip:SetSize(88, 26)
    MythicTools:ApplySurface(chip, COLORS.rowBG, COLORS.surface, nil)

    chip.Icon = chip:CreateTexture(nil, "ARTWORK")
    chip.Icon:SetSize(18, 18)
    chip.Icon:SetPoint("LEFT", 6, 0)
    chip.Icon:SetTexture(QUESTION_MARK_ICON)
    chip.Icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)

    chip.Count = CreateFont(chip, 11, COLORS.text)
    chip.Count:SetPoint("LEFT", chip.Icon, "RIGHT", 6, 0)
    chip.Count:SetJustifyH("LEFT")

    chip.Role = CreateFont(chip, 9, COLORS.subdued, "RIGHT")
    chip.Role:SetPoint("RIGHT", -6, 0)
    chip.Role:SetJustifyH("RIGHT")

    return chip
end

local function CreatePlayerDetailHistoryRow(parent)
    local row = CreateListButton(parent, PLAYER_DETAIL_HISTORY_ROW_HEIGHT - 4)
    row.IconFrame:SetSize(30, 30)
    row.IconFrame:ClearAllPoints()
    row.IconFrame:SetPoint("LEFT", 10, 0)
    row.Title:ClearAllPoints()
    row.Title:SetPoint("TOPLEFT", row.IconFrame, "TOPRIGHT", 10, -2)
    row.Title:SetPoint("TOPRIGHT", -190, -2)
    row.Subtitle:ClearAllPoints()
    row.Subtitle:SetPoint("TOPLEFT", row.Title, "BOTTOMLEFT", 0, -4)
    row.Subtitle:SetPoint("TOPRIGHT", -190, -18)
    row.RightTop:ClearAllPoints()
    row.RightTop:SetPoint("TOPRIGHT", -12, -8)
    row.RightTop:SetWidth(90)
    row.RightBottom:ClearAllPoints()
    row.RightBottom:SetPoint("BOTTOMRIGHT", -12, 8)
    row.RightBottom:SetWidth(150)
    return row
end

local function CreateRoleBadge(parent)
    local badge = CreateFrame("Frame", nil, parent)
    badge:SetSize(22, 22)
    MythicTools:ApplySurface(badge, COLORS.rowBG, COLORS.surface, nil)

    badge.Icon = badge:CreateTexture(nil, "ARTWORK")
    badge.Icon:SetPoint("TOPLEFT", 2, -2)
    badge.Icon:SetPoint("BOTTOMRIGHT", -2, 2)
    badge.Icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)

    badge.Label = CreateFont(badge, 9, COLORS.text, "CENTER")
    badge.Label:SetAllPoints()

    badge:Hide()
    return badge
end

local function CreateMultilineNoteEditor(parent, width, height)
    local holder = CreateFrame("Frame", nil, parent)
    holder:SetSize(width, height)
    MythicTools:ApplySurface(holder, COLORS.rowBG, COLORS.surface, nil)
    ClipChildren(holder)

    local editBox = CreateFrame("EditBox", nil, holder)
    editBox:SetAutoFocus(false)
    editBox:SetMultiLine(true)
    editBox:SetFontObject(ChatFontNormal)
    editBox:SetTextColor(COLORS.text[1], COLORS.text[2], COLORS.text[3], 1)
    editBox:SetMaxLetters(160)
    editBox:SetJustifyH("LEFT")
    editBox:SetJustifyV("TOP")
    editBox:SetPoint("TOPLEFT", 10, -8)
    editBox:SetPoint("BOTTOMRIGHT", -10, 8)
    editBox:SetScript("OnEscapePressed", function(self)
        self:ClearFocus()
    end)
    editBox:SetScript("OnTextChanged", function(self)
        local text = self:GetText() or ""
        if text:find("\n") then
            local firstLine, secondLine = text:match("^([^\n]*)\n([^\n]*)")
            local limited = (firstLine or "") .. "\n" .. (secondLine or "")
            if limited ~= text then
                self:SetText(limited)
                self:SetCursorPosition(strlen(limited))
            end
        end
    end)

    holder:SetScript("OnSizeChanged", function(self, newWidth, newHeight)
        editBox:SetWidth(max(20, (newWidth or width or 20) - 20))
        editBox:SetHeight(max(20, (newHeight or height or 20) - 16))
    end)

    holder.EditBox = editBox
    return holder
end

local function CreatePlayerFilterButton(parent, anchorWidget)
    local button = CreateFrame("Button", nil, parent)
    button:SetFrameLevel((parent:GetFrameLevel() or 1) + 6)
    button:SetSize(anchorWidget:GetWidth() > 0 and anchorWidget:GetWidth() or 220, 14)
    button:SetPoint("TOPLEFT", anchorWidget, "TOPLEFT", -2, 0)
    button:RegisterForClicks("AnyUp")
    button:SetHighlightTexture(WHITE_TEXTURE)

    local highlight = button:GetHighlightTexture()
    highlight:SetAllPoints()
    highlight:SetVertexColor(1, 1, 1, 0.08)

    button:SetScript("OnClick", function(self, mouseButton)
        if self.playerName and self.playerName ~= "" then
            if mouseButton == "RightButton" then
                MythicTools:ShowPlayerContextMenu(self.playerName)
            else
                MythicTools:OpenPlayerHistory(self.playerName)
            end
        end
    end)

    button:SetScript("OnEnter", function(self)
        if not self.playerName or self.playerName == "" then
            return
        end

        MythicTools:ShowPlayerSummaryTooltip(self, self.playerName, "ANCHOR_TOPLEFT")
    end)

    button:SetScript("OnLeave", function()
        GameTooltip:Hide()
    end)

    return button
end

local function CreatePortraitWidget(parent, size)
    local portrait = CreateFrame("Frame", nil, parent)
    portrait:SetSize(size, size)
    MythicTools:ApplySurface(portrait, COLORS.frameBG, COLORS.surface, nil)

    portrait.Texture = portrait:CreateTexture(nil, "ARTWORK")
    portrait.Texture:SetPoint("TOPLEFT", 1, -1)
    portrait.Texture:SetPoint("BOTTOMRIGHT", -1, 1)
    portrait.Texture:SetTexture(QUESTION_MARK_ICON)
    portrait.Texture:SetTexCoord(0, 1, 0, 1)

    portrait.SpecIcon = portrait:CreateTexture(nil, "OVERLAY")
    portrait.SpecIcon:SetSize(14, 14)
    portrait.SpecIcon:SetPoint("BOTTOMRIGHT", 4, -4)
    portrait.SpecIcon:Hide()

    return portrait
end

local function CreateLootIconButton(parent, size)
    local button = CreateFrame("Button", nil, parent)
    button:SetSize(size, size)
    button:EnableMouse(true)
    button:RegisterForClicks("AnyUp")
    button:SetFrameLevel((parent:GetFrameLevel() or 1) + 8)
    MythicTools:ApplySurface(button, COLORS.frameBG, COLORS.surface, nil)
    if button._mtSurface then
        button._mtSurface.top:SetDrawLayer("OVERLAY", 6)
        button._mtSurface.bottom:SetDrawLayer("OVERLAY", 6)
        button._mtSurface.left:SetDrawLayer("OVERLAY", 6)
        button._mtSurface.right:SetDrawLayer("OVERLAY", 6)
    end

    button.Icon = button:CreateTexture(nil, "ARTWORK")
    button.Icon:SetPoint("TOPLEFT", 1, -1)
    button.Icon:SetPoint("BOTTOMRIGHT", -1, 1)
    button.Icon:SetTexture(QUESTION_MARK_ICON)
    button.Icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)

    button.Gloss = button:CreateTexture(nil, "OVERLAY")
    button.Gloss:SetTexture(WHITE_TEXTURE)
    button.Gloss:SetPoint("TOPLEFT", 1, -1)
    button.Gloss:SetPoint("TOPRIGHT", -1, -1)
    button.Gloss:SetHeight(6)
    button.Gloss:SetVertexColor(1, 1, 1, 0.10)

    button.ItemLevel = CreateFont(button, 9, COLORS.bright, "CENTER", "OVERLAY", "OUTLINE")
    button.ItemLevel:SetPoint("BOTTOM", 0, 1)
    button.ItemLevel:SetText("")

    button:SetScript("OnEnter", function(self)
        if not self.itemLink then
            return
        end

        GameTooltip:SetOwner(self, "ANCHOR_TOPLEFT")
        GameTooltip:SetHyperlink(self.itemLink)
        GameTooltip:Show()

        self:SetScript("OnUpdate", function()
            if IsShiftKeyDown() then
                GameTooltip_ShowCompareItem(GameTooltip)
            else
                GameTooltip_HideShoppingTooltips(GameTooltip)
            end
        end)
    end)

    button:SetScript("OnLeave", function(self)
        GameTooltip:Hide()
        GameTooltip_HideShoppingTooltips(GameTooltip)
        self:SetScript("OnUpdate", nil)
    end)

    button:Hide()
    return button
end

local function CreateLootTextButton(parent, width)
    local button = CreateFrame("Button", nil, parent)
    button:SetHeight(parent:GetHeight())
    button:SetWidth(width)
    button:SetFrameLevel((parent:GetFrameLevel() or 1) + 8)
    button:EnableMouse(true)
    button:RegisterForClicks("AnyUp")

    button.Text = CreateFont(button, 10, COLORS.muted)
    button.Text:SetPoint("TOPLEFT", 0, 0)
    button.Text:SetPoint("BOTTOMRIGHT", 0, 0)
    button.Text:SetJustifyH("LEFT")
    button.Text:SetJustifyV("MIDDLE")
    button.Text:SetWordWrap(false)
    button.Text:Show()

    button:SetScript("OnEnter", function(self)
        if not self.itemLink then
            return
        end

        GameTooltip:SetOwner(self, "ANCHOR_TOPLEFT")
        GameTooltip:SetHyperlink(self.itemLink)
        GameTooltip:Show()

        self:SetScript("OnUpdate", function()
            if IsShiftKeyDown() then
                GameTooltip_ShowCompareItem(GameTooltip)
            else
                GameTooltip_HideShoppingTooltips(GameTooltip)
            end
        end)
    end)

    button:SetScript("OnLeave", function(self)
        GameTooltip:Hide()
        GameTooltip_HideShoppingTooltips(GameTooltip)
        self:SetScript("OnUpdate", nil)
    end)

    button:Hide()
    return button
end

local function CreateLootLine(parent, iconSize, textWidth)
    local line = CreateFrame("Frame", nil, parent)
    line:SetHeight(iconSize)
    line:SetWidth(iconSize + 6 + textWidth)
    line:SetClipsChildren(true)

    line.IconButton = CreateLootIconButton(line, iconSize)
    line.IconButton:SetPoint("LEFT", 0, 0)

    line.TextButton = CreateLootTextButton(line, textWidth)
    line.TextButton:SetPoint("LEFT", line.IconButton, "RIGHT", 6, 0)

    return line
end

local function CreateCompactLootStrip(parent)
    local container = CreateFrame("Frame", nil, parent)
    local width = (MAX_LOOT_ICONS * COMPACT_STATS_LOOT_ICON_SIZE) + ((MAX_LOOT_ICONS - 1) * COMPACT_STATS_LOOT_SPACING) + 34
    container:SetSize(width, COMPACT_STATS_LOOT_ICON_SIZE)

    local buttons = {}
    for index = 1, MAX_LOOT_ICONS do
        local button = CreateLootIconButton(container, COMPACT_STATS_LOOT_ICON_SIZE)
        if index == 1 then
            button:SetPoint("LEFT", 0, 0)
        else
            button:SetPoint("LEFT", buttons[index - 1], "RIGHT", COMPACT_STATS_LOOT_SPACING, 0)
        end
        buttons[index] = button
    end

    local overflow = CreateFont(container, 10, COLORS.subdued)
    overflow:SetPoint("LEFT", buttons[#buttons], "RIGHT", 6, 0)
    overflow:SetJustifyH("LEFT")
    overflow.shortFormat = true

    local empty = CreateFont(container, 10, COLORS.subdued)
    empty:SetPoint("LEFT", 0, 0)
    empty:SetJustifyH("LEFT")
    empty:SetText("No loot")

    return container, buttons, overflow, empty
end

local CreateCompletionMetricHoverButton

local function CreateCompactStatRow(parent, portraitSize, wideStats)
    local row = CreateFrame("Frame", nil, parent)
    row:SetHeight(COMPLETION_ROW_HEIGHT)
    MythicTools:ApplySurface(row, COLORS.rowBG, COLORS.surface, nil)

    row.Portrait = CreatePortraitWidget(row, portraitSize)
    row.Portrait:SetPoint("LEFT", 10, 0)

    row.PlayerText = CreateFrame("Frame", nil, row)
    row.PlayerText:SetSize(COMPACT_STATS_PLAYER_WIDTH, portraitSize)
    row.PlayerText:SetPoint("LEFT", row.Portrait, "RIGHT", 10, 0)

    row.Name = CreateFont(row.PlayerText, 12, COLORS.text)
    row.Name:SetPoint("CENTER", row.PlayerText, "CENTER", 0, 0)
    row.Name:SetWidth(COMPACT_STATS_PLAYER_WIDTH)
    row.Name:SetJustifyH("LEFT")

    row.ItemLevel = CreateFont(row.PlayerText, 10, COLORS.subdued)
    row.ItemLevel:SetPoint("TOPLEFT", row.Name, "BOTTOMLEFT", 0, -2)
    row.ItemLevel:SetWidth(COMPACT_STATS_PLAYER_WIDTH)
    row.ItemLevel:SetJustifyH("LEFT")
    row.ItemLevel:Hide()

    row.PlayerButton = CreatePlayerFilterButton(row, row.PlayerText)
    row.PlayerButton:SetSize(COMPACT_STATS_PLAYER_WIDTH + 4, portraitSize)
    row.PlayerButton:ClearAllPoints()
    row.PlayerButton:SetPoint("LEFT", row.PlayerText, "LEFT", -2, 0)

    row.Score = CreateFont(row, 11, COLORS.text)
    row.Score:SetPoint("LEFT", COMPACT_STATS_SCORE_X - COMPACT_STATS_ROW_INSET_X, 0)
    row.Score:SetWidth(88)

    row.Items, row.LootButtons, row.LootOverflow, row.LootEmpty = CreateCompactLootStrip(row)
    row.Items:SetPoint("LEFT", COMPACT_STATS_ITEMS_X - COMPACT_STATS_ROW_INSET_X, 0)
    row.LootLabel = nil
    row.LootTextButtons = nil

    row.Damage = CreateFont(row, 12, COLORS.text)
    row.Damage:SetPoint("LEFT", COMPACT_STATS_DAMAGE_X - COMPACT_STATS_ROW_INSET_X, 0)
    row.Damage:SetWidth(68)

    row.DPS = CreateFont(row, 12, COLORS.text)
    row.DPS:SetPoint("LEFT", COMPACT_STATS_DPS_X - COMPACT_STATS_ROW_INSET_X, 0)
    row.DPS:SetWidth(58)
    row.DPSHover = CreateCompletionMetricHoverButton(row, COMPACT_STATS_DPS_X, 58, "dps")

    row.Healing = CreateFont(row, 12, COLORS.text)
    row.Healing:SetPoint("LEFT", COMPACT_STATS_HEALING_X - COMPACT_STATS_ROW_INSET_X, 0)
    row.Healing:SetWidth(68)

    row.HPS = CreateFont(row, 12, COLORS.text)
    row.HPS:SetPoint("LEFT", COMPACT_STATS_HPS_X - COMPACT_STATS_ROW_INSET_X, 0)
    row.HPS:SetWidth(58)
    row.HPSHover = CreateCompletionMetricHoverButton(row, COMPACT_STATS_HPS_X, 58, "hps")

    row.Interrupts = CreateFont(row, 12, COLORS.text)
    row.Interrupts:SetPoint("LEFT", COMPACT_STATS_INTERRUPTS_X - COMPACT_STATS_ROW_INSET_X, 0)
    row.Interrupts:SetWidth(70)

    local deathsX = COMPACT_STATS_DEATHS_X
    if wideStats then
        row.Dispels = CreateFont(row, 12, COLORS.text)
        row.Dispels:SetPoint("LEFT", 914 - COMPACT_STATS_ROW_INSET_X, 0)
        row.Dispels:SetWidth(50)

        row.AvoidableDamage = CreateFont(row, 12, COLORS.text)
        row.AvoidableDamage:SetPoint("LEFT", 982 - COMPACT_STATS_ROW_INSET_X, 0)
        row.AvoidableDamage:SetWidth(96)
        row.AvoidableHover = CreateCompletionMetricHoverButton(row, 982, 96, "avoidable")

        deathsX = 1112
    end

    row.Deaths = CreateFont(row, 12, COLORS.text)
    row.Deaths:SetPoint("LEFT", deathsX - COMPACT_STATS_ROW_INSET_X, 0)
    row.Deaths:SetWidth(44)

    return row
end

local function CreateStatRow(parent)
    return CreateCompactStatRow(parent, 28)
end

local function CreateCompletionStatRow(parent)
    return CreateCompactStatRow(parent, 34, true)
end

local function CreateRecentRunButton(parent)
    return CreateListButton(parent, 54)
end

local function StyleTabButton(button, selected)
    button._selected = selected and true or false
    if selected then
        button:SetBackdropColor(COLORS.accent[1], COLORS.accent[2], COLORS.accent[3], 0.3)
        button:SetBackdropBorderColor(COLORS.accent[1], COLORS.accent[2], COLORS.accent[3], 1)
        SetTextColor(button.Text, COLORS.bright)
    else
        button:SetBackdropColor(COLORS.rowBG[1], COLORS.rowBG[2], COLORS.rowBG[3], 1)
        button:SetBackdropBorderColor(0, 0, 0, 1)
        SetTextColor(button.Text, COLORS.text)
    end
end

local function StyleListButton(button, selected, accentColor)
    button._selected = selected and true or false
    accentColor = accentColor or COLORS.accentSoft

    if selected then
        MythicTools:ApplySurface(button, COLORS.rowSelected, accentColor, COLORS.accent)
        button.Accent:ClearAllPoints()
        button.Accent:SetPoint("TOPLEFT")
        button.Accent:SetPoint("BOTTOMLEFT")
        button.Accent:SetWidth(3)
        button.Accent:SetVertexColor(accentColor[1], accentColor[2], accentColor[3], 1)
        SetTextColor(button.Title, COLORS.bright)
    else
        MythicTools:ApplySurface(button, COLORS.rowBG, COLORS.surface, nil)
        button.Accent:ClearAllPoints()
        button.Accent:SetPoint("TOPLEFT")
        button.Accent:SetPoint("BOTTOMLEFT")
        button.Accent:SetWidth(3)
        button.Accent:SetVertexColor(accentColor[1], accentColor[2], accentColor[3], 0.75)
        SetTextColor(button.Title, COLORS.text)
    end
end

local function CreateSidebarMetric(parent, labelText)
    local card = CreateFrame("Frame", nil, parent)
    card:SetSize(176, 54)
    MythicTools:ApplySurface(card, COLORS.rowBG, COLORS.surface, nil)

    card.Value = CreateFont(card, 18, COLORS.text)
    card.Value:SetPoint("TOPLEFT", 12, -10)
    card.Value:SetText("0")

    card.Label = CreateFont(card, 10, COLORS.subdued)
    card.Label:SetPoint("TOPLEFT", card.Value, "BOTTOMLEFT", 0, -3)
    card.Label:SetText(labelText)

    return card
end

local function CreateSettingToggle(parent, labelText, descriptionText, onClick)
    local row = CreateFrame("Frame", nil, parent)
    row:SetHeight(42)

    row.Check = CreateFrame("CheckButton", nil, row, "UICheckButtonTemplate")
    row.Check:SetPoint("TOPLEFT", 0, -2)
    row.Check:SetScale(0.90)
    row.Check:SetScript("OnClick", function(checkButton)
        if onClick then
            onClick(checkButton:GetChecked())
        end
    end)

    row.Label = CreateFont(row, 12, COLORS.text)
    row.Label:SetPoint("TOPLEFT", row.Check, "TOPRIGHT", 4, -1)
    row.Label:SetText(labelText)

    row.Description = CreateFont(row, 10, COLORS.subdued)
    row.Description:SetPoint("TOPLEFT", row.Label, "BOTTOMLEFT", 0, -2)
    row.Description:SetPoint("RIGHT", row, "RIGHT", -4, 0)
    row.Description:SetJustifyH("LEFT")
    row.Description:SetText(descriptionText or "")

    return row
end

function MythicTools:GetFilteredRuns()
    local filters = self.db.ui.filters
    local filtered = {}
    local normalizedDate = NormalizeDateFilter(filters.date or "")

    for _, run in ipairs(self.db.runs) do
        local include = true
        local runResult = self:GetRunResult(run)
        if include and filters.season ~= "" and filters.season ~= "all" then
            include = self:GetRunSeason(run) == filters.season
        end
        if include and filters.status ~= "all" then
            include = runResult == filters.status
        end
        if include and filters.ownedCharacters == "mine" then
            include = self:RunHasOwnedCharacter(run)
        elseif include and filters.ownedCharacters ~= "" and filters.ownedCharacters ~= "all" then
            include = self:RunHasCharacter(run, filters.ownedCharacters)
        end
        if include and filters.player ~= "" then
            local foundPlayer = false
            for _, fullName in ipairs(run.roster or {}) do
                if self:ContainsText(fullName, filters.player) or self:ContainsText(self:GetShortName(fullName), filters.player) then
                    foundPlayer = true
                    break
                end
            end
            include = foundPlayer
        end
        if include and filters.dungeon ~= "" and filters.dungeon ~= "all" then
            include = (run.dungeonName or "") == filters.dungeon
        end
        if include and normalizedDate ~= "" then
            include = self:ContainsText(self:FormatDate(run.endTime), normalizedDate)
        end
        if include and filters.search ~= "" then
            local blob = (run.dungeonName or "") .. " " .. tostring(run.level or 0) .. " " .. self:FormatDate(run.endTime) .. " " .. runResult
            for _, fullName in ipairs(run.roster or {}) do
                blob = blob .. " " .. fullName .. " " .. self:GetShortName(fullName)
            end
            include = self:ContainsText(blob, filters.search)
        end
        if include then
            filtered[#filtered + 1] = run
        end
    end

    return filtered
end

function MythicTools:RunMatchesPlayerFilters(run, filters)
    filters = filters or {}
    if filters.season and filters.season ~= "" and filters.season ~= "all" and self:GetRunSeason(run) ~= filters.season then
        return false
    end

    if filters.dungeon and filters.dungeon ~= "" and filters.dungeon ~= "all" and run.dungeonName ~= filters.dungeon then
        return false
    end

    return true
end

function MythicTools:GetFilteredRunsForPlayer(fullName, filters)
    local playerEntry = self.db.playersIndex[fullName]
    if not playerEntry then
        return {}
    end

    local runs = {}
    for _, runId in ipairs(playerEntry.runIds or {}) do
        local run = self:GetRunById(runId)
        if run and self:RunMatchesPlayerFilters(run, filters) then
            runs[#runs + 1] = run
        end
    end

    return runs
end

local function AccumulatePlayerFilterAggregate(owner, target, run, stat)
    target.totalRuns = (target.totalRuns or 0) + 1

    if run.result == "timed" then
        target.timedRuns = (target.timedRuns or 0) + 1
    elseif run.result == "abandoned" then
        target.abandonedRuns = (target.abandonedRuns or 0) + 1
    else
        target.overtimeRuns = (target.overtimeRuns or 0) + 1
    end

    local level = tonumber(run.level) or 0
    local isAbandoned = run.result == "abandoned"
    if level > 0 then
        target.bestLevel = max(target.bestLevel or 0, level)
        if run.result == "timed" then
            target.bestTimedLevel = max(target.bestTimedLevel or 0, level)
        end
        if not isAbandoned then
            target.totalLevel = (target.totalLevel or 0) + level
            target.countedLevels = (target.countedLevels or 0) + 1
        end
    end

    local dps = stat and stat.damage ~= nil and owner:GetPlayerDPS(run, stat) or nil
    if dps ~= nil and not isAbandoned then
        target.maxDps = max(target.maxDps or 0, dps)
        target.totalDps = (target.totalDps or 0) + dps
        target.countedDps = (target.countedDps or 0) + 1
    end

    local hps = stat and stat.healing ~= nil and owner:GetPlayerHPS(run, stat) or nil
    if hps ~= nil and not isAbandoned then
        target.maxHps = max(target.maxHps or 0, hps)
        target.totalHps = (target.totalHps or 0) + hps
        target.countedHps = (target.countedHps or 0) + 1
    end
end

local function FinalizePlayerFilterAggregate(target)
    if (target.totalRuns or 0) > 0 then
        target.successRate = ((target.timedRuns or 0) / target.totalRuns) * 100
    else
        target.successRate = 0
    end

    target.averageLevel = (target.countedLevels or 0) > 0 and ((target.totalLevel or 0) / target.countedLevels) or 0
    target.averageDps = (target.countedDps or 0) > 0 and ((target.totalDps or 0) / target.countedDps) or 0
    target.averageHps = (target.countedHps or 0) > 0 and ((target.totalHps or 0) / target.countedHps) or 0
end

function MythicTools:BuildFilteredPlayerAggregate(playerEntry, filters)
    local aggregate = {
        fullName = playerEntry.fullName,
        shortName = playerEntry.shortName,
        classFilename = playerEntry.classFilename,
        specIconID = playerEntry.specIconID,
        specID = playerEntry.specID,
        specName = playerEntry.specName,
        role = playerEntry.role,
        sourceEntry = playerEntry,
        totalRuns = 0,
        timedRuns = 0,
        overtimeRuns = 0,
        abandonedRuns = 0,
        bestLevel = 0,
        bestTimedLevel = 0,
        totalLevel = 0,
        countedLevels = 0,
        maxDps = 0,
        totalDps = 0,
        countedDps = 0,
        maxHps = 0,
        totalHps = 0,
        countedHps = 0,
    }

    for _, run in ipairs(self:GetFilteredRunsForPlayer(playerEntry.fullName, filters)) do
        local stat = run.playerStats and run.playerStats[playerEntry.fullName] or nil
        AccumulatePlayerFilterAggregate(self, aggregate, run, stat)
    end

    FinalizePlayerFilterAggregate(aggregate)
    return aggregate
end

function MythicTools:GetFilteredPlayers()
    local filters = self.db.ui.playerFilters or {}
    local needle = filters.search or self.db.ui.playerSearch or ""
    local players = {}

    for _, playerEntry in pairs(self.db.playersIndex) do
        if playerEntry.fullName ~= self.playerName then
            local include = true
            if include and needle ~= "" then
                include = self:ContainsText(playerEntry.fullName, needle) or self:ContainsText(playerEntry.shortName, needle)
            end
            if include then
                local aggregate = self:BuildFilteredPlayerAggregate(playerEntry, nil)
                if (aggregate.totalRuns or 0) > 0 then
                    players[#players + 1] = aggregate
                end
            end
        end
    end

    local sortState = self.db.ui.playerSort or {column = "runs", direction = "desc"}
    local direction = sortState.direction == "asc" and 1 or -1
    table.sort(players, function(left, right)
        local leftValue
        local rightValue
        if sortState.column == "player" then
            leftValue = left.shortName or left.fullName or ""
            rightValue = right.shortName or right.fullName or ""
            if leftValue ~= rightValue then
                if direction == 1 then
                    return leftValue < rightValue
                end
                return leftValue > rightValue
            end
        else
            local keyMap = {
                runs = "totalRuns",
                success = "successRate",
                timed = "timedRuns",
                overtime = "overtimeRuns",
                abandoned = "abandonedRuns",
                best = "bestTimedLevel",
                average = "averageLevel",
                maxdps = "maxDps",
                avgdps = "averageDps",
                maxhps = "maxHps",
                avghps = "averageHps",
            }
            local key = keyMap[sortState.column] or "totalRuns"
            leftValue = tonumber(left[key]) or 0
            rightValue = tonumber(right[key]) or 0
            if leftValue ~= rightValue then
                if direction == 1 then
                    return leftValue < rightValue
                end
                return leftValue > rightValue
            end
        end

        if (left.successRate or 0) ~= (right.successRate or 0) then
            return (left.successRate or 0) > (right.successRate or 0)
        end

        return (left.shortName or left.fullName or "") < (right.shortName or right.fullName or "")
    end)

    return players
end

local function GetCompletionStatValue(owner, run, stat, column)
    if column == "dps" then
        return tonumber(owner:GetPlayerDPS(run, stat)) or 0
    elseif column == "hps" then
        return tonumber(owner:GetPlayerHPS(run, stat)) or 0
    elseif column == "healing" then
        return tonumber(stat and stat.healing) or 0
    elseif column == "interrupts" then
        return tonumber(stat and stat.interrupts) or 0
    elseif column == "dispels" then
        return tonumber(stat and stat.dispels) or 0
    elseif column == "avoidableDamage" then
        return tonumber(stat and stat.avoidableDamage) or 0
    elseif column == "deaths" then
        return tonumber(stat and stat.deaths) or 0
    end

    return tonumber(stat and stat.damage) or 0
end

local function GetCompletionSortColumn(owner)
    local sortState = owner.db and owner.db.ui and owner.db.ui.completionPopupSort or nil
    local column = sortState and sortState.column or "damage"
    local validColumns = {
        damage = true,
        dps = true,
        healing = true,
        hps = true,
        interrupts = true,
        dispels = true,
        avoidableDamage = true,
        deaths = true,
    }
    if not validColumns[column] then
        return "damage"
    end

    return column
end

local function GetCompletionStatLeaders(owner, run, stats)
    local leaders = {
        dps = 0,
        hps = 0,
        interrupts = 0,
        dispels = 0,
        avoidableDamage = nil,
    }

    for _, stat in ipairs(stats or {}) do
        leaders.dps = max(leaders.dps, GetCompletionStatValue(owner, run, stat, "dps"))
        leaders.hps = max(leaders.hps, GetCompletionStatValue(owner, run, stat, "hps"))
        leaders.interrupts = max(leaders.interrupts, GetCompletionStatValue(owner, run, stat, "interrupts"))
        leaders.dispels = max(leaders.dispels, GetCompletionStatValue(owner, run, stat, "dispels"))
        if stat and stat.avoidableDamage ~= nil then
            local avoidableDamage = GetCompletionStatValue(owner, run, stat, "avoidableDamage")
            leaders.avoidableDamage = leaders.avoidableDamage == nil and avoidableDamage or min(leaders.avoidableDamage, avoidableDamage)
        end
    end

    return leaders
end

local function IsCompletionStatLeader(value, bestValue)
    value = tonumber(value) or 0
    bestValue = tonumber(bestValue) or 0
    return value > 0 and value == bestValue
end

function MythicTools:GetSortedStatsForRun(run)
    local stats = {}
    for _, fullName in ipairs(run.roster or {}) do
        local entry = run.playerStats and run.playerStats[fullName]
        if entry then
            stats[#stats + 1] = entry
        end
    end

    local sortColumn = GetCompletionSortColumn(self)
    table.sort(stats, function(left, right)
        local leftSortValue = GetCompletionStatValue(self, run, left, sortColumn)
        local rightSortValue = GetCompletionStatValue(self, run, right, sortColumn)
        if leftSortValue ~= rightSortValue then
            if sortColumn == "avoidableDamage" then
                return leftSortValue < rightSortValue
            end
            return leftSortValue > rightSortValue
        end

        local leftDamage, rightDamage = left.damage or -1, right.damage or -1
        if leftDamage ~= rightDamage then return leftDamage > rightDamage end
        local leftHealing, rightHealing = left.healing or -1, right.healing or -1
        if leftHealing ~= rightHealing then return leftHealing > rightHealing end
        local leftInterrupts, rightInterrupts = left.interrupts or -1, right.interrupts or -1
        if leftInterrupts ~= rightInterrupts then return leftInterrupts > rightInterrupts end
        return (left.name or "") < (right.name or "")
    end)

    return stats
end

function MythicTools:GetRunLootCount(run)
    local count = 0
    for _, stat in pairs(run and run.playerStats or {}) do
        count = count + #(stat.loot or {})
    end
    return count
end


local function FormatKeyLevel(level)
    level = tonumber(level) or 0
    if level <= 0 then
        return "--"
    end
    return ("+%d"):format(level)
end

local function GetPlayerNameParts(fullName)
    if type(fullName) ~= "string" or fullName == "" then
        return "", ""
    end

    local characterName, realmName = fullName:match("^([^%-]+)%-(.+)$")
    if characterName and realmName then
        return characterName, realmName
    end

    return fullName, ""
end

local function GetRaiderIORegion()
    local regionID = GetCurrentRegion and GetCurrentRegion() or nil
    local regions = {
        [1] = "us",
        [2] = "kr",
        [3] = "eu",
        [4] = "tw",
        [5] = "cn",
    }

    return regions[regionID] or "us"
end

local function GetRaiderIOSlug(text)
    if type(text) ~= "string" or text == "" then
        return nil
    end

    return text:gsub("%s+", ""):gsub("%-+", ""):lower()
end

local function BuildRaiderIOCharacterURL(fullName)
    local characterName, realmName = GetPlayerNameParts(fullName)
    local realmSlug = GetRaiderIOSlug(realmName)
    if characterName == "" or not realmSlug then
        return nil
    end

    return ("https://raider.io/characters/%s/%s/%s"):format(GetRaiderIORegion(), realmSlug, characterName)
end

local function GetRunUpgradeText(run)
    if not run or MythicTools:GetRunResult(run) == "abandoned" then
        return ""
    end

    local upgradeLevels = tonumber(run.keystoneUpgradeLevels) or 0
    if upgradeLevels <= 0 then
        return ""
    end

    return ("+%d"):format(upgradeLevels)
end

local function GetCompletionRunStatusText(run)
    local result = MythicTools:GetRunResult(run)
    if result == "timed" then
        return "Timed"
    end
    if result == "abandoned" then
        return "Abandoned"
    end
    return "Overtimed"
end

local function GetCompletionRunTitleText(run)
    local title = ("%s +%d"):format(run and run.dungeonName or "Unknown", run and run.level or 0)
    if not run then
        return title
    end

    local statusText = GetCompletionRunStatusText(run)
    local upgradeText = GetRunUpgradeText(run)
    if MythicTools:GetRunResult(run) == "timed" and upgradeText ~= "" then
        statusText = ("%s %s"):format(statusText, upgradeText)
    end

    return ("%s • %s"):format(title, ColorizeInlineText(statusText, GetRunStatusColor(run)))
end

local function FormatScoreNumber(score)
    score = tonumber(score)
    if not score then
        return ""
    end

    if math.abs(score - math.floor(score + 0.5)) < 0.05 then
        return tostring(math.floor(score + 0.5))
    end

    return ("%.1f"):format(score)
end

local function FormatScoreWithDelta(currentScore, previousScore)
    local current = tonumber(currentScore) or 0
    if current <= 0 then
        return ""
    end

    local text = FormatScoreNumber(current)
    local previous = tonumber(previousScore)
    if not previous or previous <= 0 then
        return text
    end

    local delta = current - previous
    if math.abs(delta) < 0.05 then
        return text
    end

    return ("%s(%s%s)"):format(text, delta > 0 and "+" or "", FormatScoreNumber(delta))
end

local function GetRunScoreText(run)
    local scoreText = FormatScoreWithDelta(run and run.newOverallDungeonScore, run and run.oldOverallDungeonScore)
    if scoreText ~= "" then
        return ("Score %s"):format(scoreText)
    end

    return ""
end

local function GetPlayerScoreDisplayText(stat)
    local score = tonumber(stat and stat.score) or 0
    if score > 0 then
        return FormatScoreWithDelta(score, stat and stat.previousScore)
    end

    local previousScore = tonumber(stat and stat.previousScore) or 0
    if previousScore > 0 then
        return FormatScoreNumber(previousScore)
    end

    return ""
end

local function GetPlayerScoreColor(stat)
    local score = tonumber(stat and stat.score) or tonumber(stat and stat.previousScore) or 0
    if score <= 0 or not C_ChallengeMode or not C_ChallengeMode.GetDungeonScoreRarityColor then
        return COLORS.text
    end

    local color = C_ChallengeMode.GetDungeonScoreRarityColor(score)
    if color then
        return {color.r or 1, color.g or 1, color.b or 1, color.a or 1}
    end

    return COLORS.text
end

local function FormatRunDurationSeconds(seconds)
    seconds = tonumber(seconds)
    if not seconds then
        return "--"
    end

    return MythicTools:FormatDurationMS(max(0, seconds) * 1000)
end

local function GetRunTimeRemainingText(run)
    local timeLimitSeconds = tonumber(run and run.timeLimitSeconds)
    if not timeLimitSeconds or timeLimitSeconds <= 0 then
        return "Time Remaining --"
    end

    local elapsedSeconds = (tonumber(run and run.timeMS) or 0) / 1000
    return ("Time Remaining %s"):format(FormatRunDurationSeconds(timeLimitSeconds - elapsedSeconds))
end

local function GetRunOutOfCombatDisplayText(run)
    return ("Out of combat %s"):format(FormatRunDurationSeconds(tonumber(run and run.outOfCombatSeconds) or 0))
end

local function GetRunStatsSourceText(run)
    local source = run and run.statsSource or nil

    if source == "details" then
        return "Stats Details!"
    end
    if source == "details_hybrid" then
        return "Stats Details! + Blizzard"
    end
    if source == "details_partial" then
        return "Stats Details! totals"
    end
    if source == "damage_meter" then
        return ""
    end
    if run and run.statsUnavailable then
        return "Stats unavailable"
    end

    return "Stats pending"
end

local COMPLETION_TOOLTIP_MAX_SPELLS = 7

local function GetSpellNameForTooltip(spellID)
    if C_Spell and C_Spell.GetSpellName then
        local spellName = C_Spell.GetSpellName(spellID)
        if spellName and spellName ~= "" then
            return spellName
        end
    end

    if GetSpellInfo then
        local spellName = GetSpellInfo(spellID)
        if type(spellName) == "string" and spellName ~= "" then
            return spellName
        end
    end

    return ("Spell #%s"):format(tostring(spellID or "?"))
end

local function GetSpellIconForTooltip(spellID)
    if C_Spell and C_Spell.GetSpellTexture then
        local icon = C_Spell.GetSpellTexture(spellID)
        if icon then
            return icon
        end
    end

    if GetSpellInfo then
        local _, _, icon = GetSpellInfo(spellID)
        if icon then
            return icon
        end
    end

    return nil
end

local function GetCompletionMetricTooltipData(metricKey, stat)
    if metricKey == "dps" then
        return stat and stat.damageDoneBySpells or nil, "Damage Skills"
    elseif metricKey == "hps" then
        return stat and stat.healDoneBySpells or nil, "Healing Skills"
    elseif metricKey == "avoidable" then
        return stat and stat.avoidableDamageBySpells or nil, "Avoidable Damage Skills"
    end

    return nil, nil
end

local function ShowCompletionMetricTooltip(button)
    local stat = button and button.playerStat or nil
    local metricKey = button and button.metricKey or nil
    if not stat or not metricKey then
        return
    end

    local spells, title = GetCompletionMetricTooltipData(metricKey, stat)
    if type(spells) ~= "table" or #spells == 0 then
        return
    end

    local classColor = stat.classFilename and RAID_CLASS_COLORS and RAID_CLASS_COLORS[stat.classFilename]
    local headerName = stat.shortName or stat.name or "Unknown"

    GameTooltip:SetOwner(button, "ANCHOR_RIGHT")
    GameTooltip:ClearLines()
    if classColor then
        GameTooltip:AddLine(headerName, classColor.r, classColor.g, classColor.b)
    else
        GameTooltip:AddLine(headerName, 1, 1, 1)
    end
    GameTooltip:AddLine(title or "Skills", 1, 0.82, 0)
    GameTooltip:AddLine(" ")

    local count = 0
    for _, spellData in ipairs(spells) do
        count = count + 1
        if count > COMPLETION_TOOLTIP_MAX_SPELLS then
            break
        end

        local spellID = tonumber(spellData.spellID) or tonumber(spellData.spellId) or tonumber(spellData.id) or 0
        local totalAmount = tonumber(spellData.totalAmount) or tonumber(spellData.total) or tonumber(spellData.amount) or 0
        local spellName = GetSpellNameForTooltip(spellID)
        local spellIcon = GetSpellIconForTooltip(spellID)
        local iconMarkup = spellIcon and ("|T%s:14:14:0:0:64:64:5:59:5:59|t "):format(tostring(spellIcon)) or ""
        GameTooltip:AddDoubleLine(iconMarkup .. spellName, MythicTools:FormatAmount(totalAmount), 1, 1, 1, 1, 1, 1)
    end

    GameTooltip:Show()
end

CreateCompletionMetricHoverButton = function(row, x, width, metricKey)
    local hoverButton = CreateFrame("Button", nil, row)
    hoverButton:SetFrameLevel((row:GetFrameLevel() or 1) + 5)
    hoverButton:SetPoint("TOPLEFT", x - COMPACT_STATS_ROW_INSET_X, 0)
    hoverButton:SetSize(width, COMPLETION_ROW_HEIGHT)
    hoverButton.metricKey = metricKey
    hoverButton:SetScript("OnEnter", ShowCompletionMetricTooltip)
    hoverButton:SetScript("OnLeave", function()
        GameTooltip:Hide()
    end)
    return hoverButton
end

local function PlayerTableCellUpdate(rowFrame, cellFrame, data, cols, row, realrow, column, fShow, table)
    if not fShow then
        if cellFrame.NoteIcon then
            cellFrame.NoteIcon:Hide()
        end
        cellFrame:Hide()
        return
    end

    local rowData = table:GetRow(realrow)
    local cellData = table:GetCell(rowData, column)
    local isNoteColumn = cols and cols[column] and cols[column].noteColumn
    local displayValue = ""
    local color = COLORS.text

    if type(cellData) == "table" then
        displayValue = cellData.display or cellData.value or ""
        color = cellData.color or COLORS.text
    else
        displayValue = cellData or ""
    end

    if isNoteColumn then
        cellFrame.text:SetText("")
        if not cellFrame.NoteIcon then
            cellFrame.NoteIcon = cellFrame:CreateTexture(nil, "OVERLAY", nil, 7)
            cellFrame.NoteIcon:SetTexture(PLAYER_NOTE_ICON)
            cellFrame.NoteIcon:SetBlendMode("ADD")
            cellFrame.NoteIcon:SetTexCoord(0, 1, 0, 1)
            cellFrame.NoteIcon:SetSize(16, 16)
            cellFrame.NoteIcon:SetPoint("CENTER", cellFrame, "CENTER", 0, 0)
        end
        cellFrame.NoteIcon:SetShown(type(cellData) == "table" and cellData.noteText ~= nil and cellData.noteText ~= "")
        if rowData and rowData.playerName and type(cellData) == "table" and cellData.noteText ~= nil and cellData.noteText ~= "" then
            cellFrame:SetScript("OnEnter", function(self)
                MythicTools:ShowPlayerSummaryTooltip(self, rowData.playerName, "ANCHOR_RIGHT")
            end)
            cellFrame:SetScript("OnLeave", function()
                GameTooltip:Hide()
            end)
        else
            cellFrame:SetScript("OnEnter", nil)
            cellFrame:SetScript("OnLeave", nil)
        end
    else
        if cellFrame.NoteIcon then
            cellFrame.NoteIcon:Hide()
        end
        cellFrame:SetScript("OnEnter", nil)
        cellFrame:SetScript("OnLeave", nil)
        cellFrame.text:SetText(displayValue)
    end
    cellFrame.text:SetTextColor(color[1], color[2], color[3], color[4] or 1)
    cellFrame:Show()
end

function MythicTools:BuildPlayerAnalyticsTableData(players)
    local rows = {}

    for _, playerEntry in ipairs(players or {}) do
        local iconMarkup = GetClassIconMarkup(playerEntry.classFilename, playerEntry.specIconID, playerEntry.specID)
        local noteText = self:GetPlayerNote(playerEntry.fullName)
        local hasNote = noteText ~= nil and noteText ~= ""
        rows[#rows + 1] = {
            playerName = playerEntry.fullName,
            cols = {
                {value = playerEntry.shortName or playerEntry.fullName or "", display = iconMarkup},
                {value = playerEntry.shortName or playerEntry.fullName or "", display = playerEntry.shortName or playerEntry.fullName or "", color = COLORS.text},
                {value = playerEntry.totalRuns or 0, display = tostring(playerEntry.totalRuns or 0)},
                {value = playerEntry.successRate or 0, display = string.format("%.1f%%", playerEntry.successRate or 0)},
                {value = playerEntry.timedRuns or 0, display = tostring(playerEntry.timedRuns or 0), color = COLORS.success},
                {value = playerEntry.overtimeRuns or 0, display = tostring(playerEntry.overtimeRuns or 0), color = COLORS.accentSoft},
                {value = playerEntry.abandonedRuns or 0, display = tostring(playerEntry.abandonedRuns or 0), color = COLORS.danger},
                {value = playerEntry.bestTimedLevel or 0, display = tostring(playerEntry.bestTimedLevel or 0)},
                {value = playerEntry.averageLevel or 0, display = string.format("%.1f", playerEntry.averageLevel or 0)},
                {value = playerEntry.maxDps or 0, display = self:FormatAmount(playerEntry.maxDps or 0)},
                {value = playerEntry.averageDps or 0, display = self:FormatAmount(playerEntry.averageDps or 0)},
                {value = playerEntry.maxHps or 0, display = self:FormatAmount(playerEntry.maxHps or 0)},
                {value = playerEntry.averageHps or 0, display = self:FormatAmount(playerEntry.averageHps or 0)},
                {value = hasNote and 1 or 0, display = "", noteText = hasNote and noteText or nil},
            }
        }
    end

    return rows
end

function MythicTools:CreatePlayersAnalyticsTable(parent)
    if not ScrollingTable then
        return nil
    end

    local columns = {
        {name = "", width = 30, sort = nil, DoCellUpdate = PlayerTableCellUpdate},
        {name = "Player", width = 160, DoCellUpdate = PlayerTableCellUpdate},
        {name = "Runs", width = 52, defaultsort = ScrollingTable.SORT_DSC, DoCellUpdate = PlayerTableCellUpdate},
        {name = "Success", width = 74, DoCellUpdate = PlayerTableCellUpdate},
        {name = "Timed", width = 52, DoCellUpdate = PlayerTableCellUpdate},
        {name = "Overtime", width = 66, DoCellUpdate = PlayerTableCellUpdate},
        {name = "Abandoned", width = 78, DoCellUpdate = PlayerTableCellUpdate},
        {name = "Best", width = 48, DoCellUpdate = PlayerTableCellUpdate},
        {name = "Average", width = 58, DoCellUpdate = PlayerTableCellUpdate},
        {name = "Max DPS", width = 82, DoCellUpdate = PlayerTableCellUpdate},
        {name = "Avg DPS", width = 82, DoCellUpdate = PlayerTableCellUpdate},
        {name = "Max HPS", width = 82, DoCellUpdate = PlayerTableCellUpdate},
        {name = "Avg HPS", width = 82, DoCellUpdate = PlayerTableCellUpdate},
        {name = "", width = 26, sort = nil, DoCellUpdate = PlayerTableCellUpdate, noteColumn = true, align = "CENTER"},
    }

    local windowHeight = (MythicTools.db and MythicTools.db.ui and MythicTools.db.ui.height) or 760
    local numRows = max(10, floor((windowHeight - 234) / PLAYER_TABLE_ROW_HEIGHT))
    local tableWidget = ScrollingTable:CreateST(columns, numRows, PLAYER_TABLE_ROW_HEIGHT, nil, parent)
    tableWidget.frame:SetPoint("TOPLEFT", parent, "TOPLEFT", 12, -46)
    tableWidget.frame:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -12, -46)
    tableWidget.frame:SetBackdropColor(COLORS.panelBG[1], COLORS.panelBG[2], COLORS.panelBG[3], 0)
    tableWidget.frame:SetBackdropBorderColor(0, 0, 0, 0)
    tableWidget:EnableSelection(true)

    local sortColumns = {
        [1] = "player",
        [2] = "player",
        [3] = "runs",
        [4] = "success",
        [5] = "timed",
        [6] = "overtime",
        [7] = "abandoned",
        [8] = "best",
        [9] = "average",
        [10] = "maxdps",
        [11] = "avgdps",
        [12] = "maxhps",
        [13] = "avghps",
    }

    tableWidget:RegisterEvents({
        OnEnter = function(rowFrame, cellFrame, data, cols, row, realrow, column, scrollingTable)
            if not (cols and cols[column] and cols[column].noteColumn) then
                return false
            end

            local rowData = scrollingTable and scrollingTable:GetRow(realrow)
            local cellData = rowData and scrollingTable:GetCell(rowData, column)
            local noteText = type(cellData) == "table" and cellData.noteText or nil
            if rowData and rowData.playerName and noteText and noteText ~= "" then
                MythicTools:ShowPlayerSummaryTooltip(cellFrame, rowData.playerName, "ANCHOR_RIGHT")
            end
            return false
        end,
        OnLeave = function()
            GameTooltip:Hide()
            return false
        end,
        OnClick = function(_, _, data, _, row, realrow, column, scrollingTable, mouseButton)
            if row and realrow then
                local rowData = data and data[realrow]
                if rowData and rowData.playerName then
                    if mouseButton == "RightButton" then
                        MythicTools:ShowPlayerContextMenu(rowData.playerName)
                    else
                        MythicTools:OpenPlayerHistory(rowData.playerName)
                    end
                end
                return true
            end

            if mouseButton == "RightButton" then
                return false
            end

            C_Timer.After(0, function()
                local sortColumn = sortColumns[column]
                if not sortColumn then
                    return
                end

                local sortDirection = "desc"
                if scrollingTable and scrollingTable.cols and scrollingTable.cols[column] and scrollingTable.cols[column].sort == ScrollingTable.SORT_ASC then
                    sortDirection = "asc"
                end
                MythicTools.db.ui.playerSort = {
                    column = sortColumn,
                    direction = sortDirection,
                }
            end)

            return false
        end,
    }, true)

    return tableWidget
end

function MythicTools:ApplyPlayersTableSortState()
    if not (self.ui and self.ui.playersTable and self.ui.playersTable.cols and ScrollingTable) then
        return
    end

    local sortState = self.db.ui.playerSort or {column = "runs", direction = "desc"}
    local columnIndexByKey = {
        player = 2,
        runs = 3,
        success = 4,
        timed = 5,
        overtime = 6,
        abandoned = 7,
        best = 8,
        average = 9,
        maxdps = 10,
        avgdps = 11,
        maxhps = 12,
        avghps = 13,
    }
    local selectedIndex = columnIndexByKey[sortState.column] or 3

    for index, column in ipairs(self.ui.playersTable.cols) do
        if index == selectedIndex then
            column.sort = sortState.direction == "asc" and ScrollingTable.SORT_ASC or ScrollingTable.SORT_DSC
        else
            column.sort = nil
        end
    end
end

function MythicTools:GetSelectedPlayerEntry()
    local selectedPlayer = self.db and self.db.ui and self.db.ui.selectedPlayer
    if not selectedPlayer then
        return nil
    end

    return self.db.playersIndex and self.db.playersIndex[selectedPlayer] or nil
end

function MythicTools:GetPlayerRecentRuns(fullName, limit, filters)
    local runs = filters and self:GetFilteredRunsForPlayer(fullName, filters) or self:GetRunsForPlayer(fullName)
    table.sort(runs, function(left, right)
        return (left.endTime or 0) > (right.endTime or 0)
    end)

    if limit and #runs > limit then
        local trimmed = {}
        for index = 1, limit do
            trimmed[index] = runs[index]
        end
        return trimmed
    end

    return runs
end

function MythicTools:GetVisiblePlayerDetailHistoryRows()
    local historyBody = self.ui and self.ui.playerDetailHistoryBody
    local availableHeight = historyBody and historyBody:GetHeight() or 0
    if availableHeight <= 0 then
        return PLAYER_DETAIL_HISTORY_ROWS
    end

    return max(1, min(PLAYER_DETAIL_HISTORY_MAX_ROWS, floor((availableHeight + 2) / PLAYER_DETAIL_HISTORY_ROW_HEIGHT)))
end

function MythicTools:GetSortedBreakdownEntries(breakdown)
    local rows = {}
    for _, entry in pairs(breakdown or {}) do
        rows[#rows + 1] = entry
    end

    table.sort(rows, function(left, right)
        if (left.totalRuns or 0) ~= (right.totalRuns or 0) then
            return (left.totalRuns or 0) > (right.totalRuns or 0)
        end
        if (left.bestTimedLevel or 0) ~= (right.bestTimedLevel or 0) then
            return (left.bestTimedLevel or 0) > (right.bestTimedLevel or 0)
        end
        return (left.label or "") < (right.label or "")
    end)

    return rows
end

function MythicTools:SetMetricCardValue(card, value, meta, color)
    if not card then
        return
    end

    card.Value:SetText(value or "--")
    if color then
        SetTextColor(card.Value, color)
    else
        SetTextColor(card.Value, COLORS.text)
    end

    if meta and meta ~= "" then
        card.Meta:SetText(meta)
        card.Meta:Show()
    else
        card.Meta:SetText("")
        card.Meta:Hide()
    end
end

function MythicTools:GetDungeonFilterLabel(value)
    local options = GetDungeonOptionsForSeason((self.db.ui.filters and self.db.ui.filters.season) or SEASON_OPTIONS[1].value)
    for _, info in ipairs(options) do
        if info.value == value then
            return info.label
        end
    end
    return options[1] and options[1].label or "All dungeons"
end

function MythicTools:GetNextDungeonFilterValue(currentValue)
    local options = GetDungeonOptionsForSeason((self.db.ui.filters and self.db.ui.filters.season) or SEASON_OPTIONS[1].value)
    for index, info in ipairs(options) do
        if info.value == currentValue then
            local nextInfo = options[index + 1] or options[1]
            return nextInfo.value
        end
    end
    return options[1] and options[1].value or "all"
end

function MythicTools:SetRunViewMode(mode)
    self.db.ui.runViewMode = mode == "detail" and "detail" or "list"
end

function MythicTools:OpenRunDetails(runId)
    if not runId then
        return
    end

    self.db.ui.selectedRunId = runId
    self:SetRunViewMode("detail")
    if self.ui then
        self:RefreshRunView()
    end
end
function MythicTools:StoreMainFramePoint()
    if not (self.ui and self.ui.frame) then
        return
    end

    local point, _, relativePoint, xOfs, yOfs = self.ui.frame:GetPoint(1)
    self.db.ui.point = {point or "CENTER", relativePoint or point or "CENTER", xOfs or 0, yOfs or 0}
end

function MythicTools:SetPortraitWidget(widget, fullName, classFilename, specIconID, specID)
    if not widget then
        return
    end

    local texture = widget.Texture
    local usedPortrait = false
    local unit = self.GetGroupUnitForPlayer and self:GetGroupUnitForPlayer(fullName) or nil
    if unit and texture then
        texture:SetTexture(nil)
        SetPortraitTexture(texture, unit)
        if texture:GetTexture() then
            texture:SetTexCoord(0, 0, 0, 1, 1, 0, 1, 1)
            usedPortrait = true
        end
    end

    if not usedPortrait and texture then
        if specID or (specIconID and not classFilename) then
            texture:SetTexture(self:ResolveDisplayIcon(specIconID, specID))
            texture:SetTexCoord(0.08, 0.92, 0.08, 0.92)
        else
            local coords = classFilename and CLASS_ICON_TCOORDS and CLASS_ICON_TCOORDS[classFilename]
            if coords then
                texture:SetTexture(CLASS_CIRCLE_TEXTURE)
                texture:SetTexCoord(coords[1], coords[2], coords[3], coords[4])
            else
                texture:SetTexture(QUESTION_MARK_ICON)
                texture:SetTexCoord(0, 1, 0, 1)
            end
        end
    end

    if usedPortrait and (specID or (specIconID and not classFilename)) then
        widget.SpecIcon:SetTexture(self:ResolveDisplayIcon(specIconID, specID))
        widget.SpecIcon:Show()
    else
        widget.SpecIcon:Hide()
    end
end

function MythicTools:SetRoleBadge(badge, role)
    if not badge then
        return
    end

    local normalizedRole = self:NormalizeRole(role)
    if not normalizedRole then
        badge:Hide()
        return
    end

    local atlasByRole = {
        TANK = "roleicon-tiny-tank",
        HEALER = "roleicon-tiny-healer",
        DAMAGER = "roleicon-tiny-dps",
    }

    badge.Icon:SetTexture(nil)
    badge.Icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
    badge.Label:Hide()

    local atlas = atlasByRole[normalizedRole]
    if atlas and badge.Icon.SetAtlas and pcall(badge.Icon.SetAtlas, badge.Icon, atlas, true) then
        badge:Show()
        return
    end

    if GetTexCoordsForRoleSmall then
        local left, right, top, bottom = GetTexCoordsForRoleSmall(normalizedRole)
        badge.Icon:SetAtlas(nil)
        badge.Icon:SetTexture(ROLE_ICON_TEXTURE)
        badge.Icon:SetTexCoord(left, right, top, bottom)
        badge:Show()
        return
    end

    badge.Icon:SetAtlas(nil)
    badge.Icon:SetTexture(nil)
    badge.Label:SetText(normalizedRole == "HEALER" and "H" or (normalizedRole == "TANK" and "T" or "D"))
    badge.Label:Show()
    badge:Show()
end

function MythicTools:HandleItemInfoReceived(_, success)
    if success == false then
        return
    end

    local mainShown = self.ui and self.ui.frame and self.ui.frame:IsShown()
    local popupShown = self.completionPopup and self.completionPopup:IsShown()
    if mainShown or popupShown then
        self:RefreshAllViews()
    end
end

function MythicTools:SetRunArt(iconWidget, backgroundWidget, run, backgroundLayout)
    local icon = GetDungeonIcon(run)
    if iconWidget then
        iconWidget:SetTexture(icon or DEFAULT_DUNGEON_ICON)
        iconWidget:SetTexCoord(0.08, 0.92, 0.08, 0.92)
    end

    if backgroundWidget then
        local bg = GetDungeonBackground(run)
        backgroundWidget:SetTexture(bg or DEFAULT_DUNGEON_BG)
        if backgroundLayout == "cover" then
            SetTextureCoverTexCoord(backgroundWidget, DUNGEON_HEADER_BACKGROUND_ASPECT)
        else
            backgroundWidget:SetTexCoord(0, 1, 0, 1)
        end
        if backgroundWidget.SetHorizTile then
            backgroundWidget:SetHorizTile(false)
        end
        if backgroundWidget.SetVertTile then
            backgroundWidget:SetVertTile(false)
        end
    end
end

function MythicTools:ResetLootButtons(buttons, lootLabel, overflowLabel, emptyLabel, textButtons)
    if type(buttons) == "table" then
        for _, button in ipairs(buttons) do
            button.itemLink = nil
            button.itemLevel = nil
            button:SetScript("OnUpdate", nil)
            button.Icon:SetTexture(QUESTION_MARK_ICON)
            button.Icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
            if button.ItemLevel then
                button.ItemLevel:SetText("")
            end
            self:ApplySurface(button, COLORS.frameBG, COLORS.surface, nil)
            button:Hide()
        end
    end

    if type(textButtons) == "table" then
        for _, button in ipairs(textButtons) do
            button.itemLink = nil
            button.Text:SetText("")
            button.Text:Show()
            button:Hide()
        end
    end

    if lootLabel and (type(textButtons) ~= "table" or #textButtons == 0) then
        lootLabel:SetText("")
        lootLabel:Hide()
    end

    if overflowLabel then
        overflowLabel:SetText("")
        overflowLabel:Hide()
    end

    if emptyLabel then
        emptyLabel:Show()
    end
end

function MythicTools:SetLootButtons(buttons, lootLabel, overflowLabel, emptyLabel, lootLinks, textButtons)
    self:ResetLootButtons(buttons, lootLabel, overflowLabel, emptyLabel, textButtons)

    if self.db and self.db.settings and self.db.settings.showLootHistory == false then
        if emptyLabel then
            emptyLabel:SetText("Loot hidden")
            emptyLabel:Show()
        end
        return
    end

    if type(lootLinks) ~= "table" or #lootLinks == 0 then
        return
    end

    if emptyLabel then
        emptyLabel:SetText("No loot")
        emptyLabel:Hide()
    end

    local visibleCount = min(#(buttons or {}), #lootLinks)
    for index = 1, visibleCount do
        local button = buttons[index]
        local textButton = textButtons and textButtons[index]
        local itemLink = NormalizeDisplayItemLink(lootLinks[index])
        if button and itemLink then
            local displayName = GetItemNameFallback(itemLink)
            if displayName == "" then
                displayName = "Unknown item"
            end

            button.itemLink = itemLink
            if textButton then
                textButton.itemLink = itemLink
            end

            local itemID = GetItemIDFromLinkish(itemLink)
            local icon = C_Item and C_Item.GetItemInfoInstant and select(5, C_Item.GetItemInfoInstant(itemLink)) or nil
            if (not icon or icon == 0) and itemID and C_Item and C_Item.GetItemIconByID then
                icon = C_Item.GetItemIconByID(itemID)
            end
            local itemLevel = GetItemLevelForLink(itemLink)
            button.Icon:SetTexture(icon or QUESTION_MARK_ICON)
            button.Icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
            button.itemLevel = itemLevel
            if button.ItemLevel then
                button.ItemLevel:SetText(FormatItemLevelText(itemLevel))
            end
            button:Show()

            local qualityColor = GetItemQualityColorByLink(itemLink) or COLORS.muted
            MythicTools:ApplySurface(button, COLORS.frameBG, qualityColor, nil)

            if textButton then
                textButton.Text:SetText(displayName)
                textButton.Text:Show()
                SetTextColor(textButton.Text, qualityColor)
                textButton:Show()
            elseif index == 1 and lootLabel then
                lootLabel:SetText(displayName)
                SetTextColor(lootLabel, qualityColor)
                lootLabel:Show()
            end

            if Item and Item.CreateFromItemLink then
                local item = Item:CreateFromItemLink(itemLink)
                item:ContinueOnItemLoad(function()
                    if button.itemLink ~= itemLink then
                        return
                    end

                    local quality = item.GetItemQuality and item:GetItemQuality() or nil
                    local loadedIcon = item.GetItemIcon and item:GetItemIcon() or icon
                    button.Icon:SetTexture(loadedIcon or QUESTION_MARK_ICON)
                    if button.ItemLevel then
                        local loadedItemLevel = (item.GetCurrentItemLevel and item:GetCurrentItemLevel()) or GetItemLevelForLink(itemLink)
                        button.itemLevel = tonumber(loadedItemLevel) or button.itemLevel
                        button.ItemLevel:SetText(FormatItemLevelText(button.itemLevel))
                    end

                    local targetText = textButton and textButton.Text or lootLabel
                    if targetText then
                        targetText:SetText(item.GetItemName and item:GetItemName() or displayName)
                        targetText:Show()
                    end

                    local border = COLORS.surface
                    if quality and C_Item.GetItemQualityColor then
                        local r, g, b = C_Item.GetItemQualityColor(quality)
                        border = {r, g, b, 1}
                        if targetText then
                            SetTextColor(targetText, border)
                        end
                    else
                        if targetText then
                            SetTextColor(targetText, COLORS.muted)
                        end
                    end
                    MythicTools:ApplySurface(button, COLORS.frameBG, border, nil)

                end)
            end
        end
    end

    if overflowLabel and #lootLinks > visibleCount then
        if overflowLabel.shortFormat then
            overflowLabel:SetText(("+%d"):format(#lootLinks - visibleCount))
        else
            overflowLabel:SetText(("+%d more item%s"):format(#lootLinks - visibleCount, (#lootLinks - visibleCount) == 1 and "" or "s"))
        end
        overflowLabel:Show()
    end
end

function MythicTools:GetCompletionPopupDelay()
    return self:Clamp(self.db.settings.completionPopupDelay or 0, 0, 10)
end

function MythicTools:GetCompletionPopupScale()
    return self:Clamp(self.db.settings.completionPopupScale or 1, 0.7, 1.4)
end

function MythicTools:ApplyCompletionPopupScale()
    if self.completionPopup then
        self.completionPopup:SetScale(self:GetCompletionPopupScale())
    end
end

function MythicTools:QueueCompletionPopup(runId, reason)
    if not runId or not (self.db and self.db.settings) then
        return
    end

    if self.db.settings.showCompletionPopup == false then
        if self.PushRuntimeDebug then
            self:PushRuntimeDebug(("skip completion popup run %s reason=%s popup disabled"):format(tostring(runId), tostring(reason)))
        end
        return
    end

    local trigger = self.db.settings.completionPopupTrigger or "LOOT_OPENED"
    local delay = self:GetCompletionPopupDelay()
    if self.PushRuntimeDebug then
        self:PushRuntimeDebug(("queue completion popup run %s trigger=%s reason=%s delay=%s"):format(
            tostring(runId),
            tostring(trigger),
            tostring(reason),
            tostring(delay)
        ))
    end

    local effectiveReason = reason
    local lootTracking = self.runtime and self.runtime.lootTracking or nil
    if trigger == "LOOT_OPENED"
        and reason == "COMPLETED"
        and type(lootTracking) == "table"
        and lootTracking.runId == runId
        and lootTracking.lootOpened then
        effectiveReason = "LOOT_OPENED"
    end

    if trigger == effectiveReason then
        self.runtime.pendingCompletionPopup = nil
        local function OpenPopupNow()
            if MythicTools.db and MythicTools.db.settings.showCompletionPopup ~= false then
                local ok, err = pcall(MythicTools.ShowCompletionPopup, MythicTools, runId)
                if not ok and MythicTools.StoreDebugReport then
                    MythicTools:StoreDebugReport("popup_show_error", {
                        trigger = effectiveReason,
                        runId = runId,
                        error = tostring(err),
                    })
                end
            end
        end

        if delay <= 0 then
            OpenPopupNow()
        else
            self:Schedule(delay, OpenPopupNow)
        end
    else
        self.runtime.pendingCompletionPopup = {runId = runId}
        if self.PushRuntimeDebug then
            self:PushRuntimeDebug(("store pending completion popup run %s waiting for %s"):format(tostring(runId), tostring(trigger)))
        end
    end
end

function MythicTools:ResolvePendingCompletionPopup(reason)
    if not (self.runtime and self.runtime.pendingCompletionPopup) then
        return
    end

    local trigger = self.db.settings.completionPopupTrigger or "LOOT_OPENED"
    if trigger ~= reason then
        return
    end

    local pending = self.runtime.pendingCompletionPopup
    self.runtime.pendingCompletionPopup = nil
    local delay = self:GetCompletionPopupDelay()
    if self.PushRuntimeDebug then
        self:PushRuntimeDebug(("resolve pending completion popup run %s reason=%s delay=%s"):format(
            tostring(pending.runId),
            tostring(reason),
            tostring(delay)
        ))
    end

    local function OpenPopupNow()
        if MythicTools.db and MythicTools.db.settings.showCompletionPopup ~= false then
            local ok, err = pcall(MythicTools.ShowCompletionPopup, MythicTools, pending.runId)
            if not ok and MythicTools.StoreDebugReport then
                MythicTools:StoreDebugReport("popup_show_error", {
                    trigger = reason,
                    runId = pending.runId,
                    error = tostring(err),
                })
            end
        end
    end

    if delay <= 0 then
        OpenPopupNow()
    else
        self:Schedule(delay, OpenPopupNow)
    end
end

function MythicTools:BuildTestRun()
    local now = time()
    local roster = {
        "Arkanis-Azralon",
        "Lumera-Stormrage",
        "Kaorth-Illidan",
        "Brielle-Sargeras",
        "Torvak-Ragnaros",
    }

    local playerStats = {
        [roster[1]] = {name = roster[1], shortName = "Arkanis", classFilename = "MAGE", damage = 20450000, healing = 310000, interrupts = 4, dispels = 0, avoidableDamage = 210000, deaths = 0, loot = {"|cff0070dd|Hitem:19019::::::::80:::::|h[Thunderfury Test]|h|r"}},
        [roster[2]] = {name = roster[2], shortName = "Lumera", classFilename = "PRIEST", damage = 5810000, healing = 15880000, interrupts = 1, dispels = 5, avoidableDamage = 78000, deaths = 1, loot = {"|cff1eff00|Hitem:17182::::::::80:::::|h[Sulfuras Cloak Test]|h|r"}},
        [roster[3]] = {name = roster[3], shortName = "Kaorth", classFilename = "WARRIOR", damage = 16800000, healing = 0, interrupts = 8, dispels = 0, avoidableDamage = 0, deaths = 0, loot = {}},
        [roster[4]] = {name = roster[4], shortName = "Brielle", classFilename = "DRUID", damage = 12150000, healing = 4320000, interrupts = 2, dispels = 2, avoidableDamage = 510000, deaths = 2, loot = {"|cffa335ee|Hitem:18803::::::::80:::::|h[Finkle's Lava Dredger Test]|h|r", "|cff0070dd|Hitem:19362::::::::80:::::|h[Doom's Edge Test]|h|r"}},
        [roster[5]] = {name = roster[5], shortName = "Torvak", classFilename = "PALADIN", damage = 10900000, healing = 2140000, interrupts = 3, dispels = 1, avoidableDamage = 125000, deaths = 0, loot = {"|cffa335ee|Hitem:19364::::::::80:::::|h[Ashkandi Test]|h|r"}},
    }

    return {
        runId = -1,
        preview = true,
        startTime = now - 1600,
        endTime = now,
        mapChallengeModeID = 1,
        mapID = 1,
        dungeonName = "Preview Dungeon",
        level = 12,
        timeMS = 1423000,
        onTime = true,
        keystoneUpgradeLevels = 2,
        practiceRun = false,
        timeLimitSeconds = 1560,
        totalDeaths = 3,
        deathPenaltySeconds = 15,
        roster = roster,
        playerStats = playerStats,
        statsUnavailable = false,
        statsNote = "Layout preview: tooltip, loot, and portraits.",
        textureFileID = DEFAULT_DUNGEON_ICON,
        backgroundTextureFileID = DEFAULT_DUNGEON_BG,
    }
end

function MythicTools:ShowTestCompletionPopup()
    self:ShowCompletionPopup(self:BuildTestRun())
end

function MythicTools:CSVCell(value)
    if value == nil then
        return ""
    end

    if type(value) == "boolean" then
        value = value and "true" or "false"
    else
        value = tostring(value)
    end

    value = value:gsub("\r\n", "\n"):gsub("\r", "\n")
    if value:find("[\",\n]") then
        value = "\"" .. value:gsub("\"", "\"\"") .. "\""
    end

    return value
end

function MythicTools:CSVRow(values, count)
    local cells = {}
    local limit = count or #values
    for index = 1, limit do
        cells[index] = self:CSVCell(values[index])
    end
    return table.concat(cells, ",")
end

function MythicTools:BuildRawCSVExport()
    local columns = {
        "run_id", "start_time", "start_datetime", "end_time", "end_datetime",
        "owner_character", "dungeon_name", "map_challenge_mode_id", "map_id",
        "season", "level", "result", "timed_upgrade", "time_ms",
        "time_limit_seconds", "total_deaths", "death_penalty_seconds",
        "old_overall_dungeon_score", "new_overall_dungeon_score",
        "stats_source", "stats_unavailable", "stats_note",
        "player_name", "player_short_name", "class", "spec_id", "spec_name",
        "role", "score", "previous_score", "item_level", "damage", "dps",
        "healing", "hps", "interrupts", "dispels", "avoidable_damage",
        "player_deaths", "loot",
    }
    local columnCount = #columns
    local rows = {self:CSVRow(columns, columnCount)}

    for _, run in ipairs(self.db and self.db.runs or {}) do
        if type(run) == "table" then
            local roster = self:NormalizeRunRoster(run)
            local timedUpgrade = self:GetRunResult(run) == "timed" and tonumber(run.keystoneUpgradeLevels) or nil
            if timedUpgrade and timedUpgrade <= 0 then
                timedUpgrade = nil
            end

            if #roster == 0 then
                rows[#rows + 1] = self:CSVRow({
                    run.runId, run.startTime, self:FormatDateTime(run.startTime),
                    run.endTime, self:FormatDateTime(run.endTime), run.ownerCharacter,
                    run.dungeonName, run.mapChallengeModeID, run.mapID, run.season,
                    run.level, self:GetRunResult(run), timedUpgrade, run.timeMS,
                    run.timeLimitSeconds, run.totalDeaths, run.deathPenaltySeconds,
                    run.oldOverallDungeonScore, run.newOverallDungeonScore,
                    run.statsSource, run.statsUnavailable and true or false, run.statsNote,
                }, columnCount)
            else
                for _, fullName in ipairs(roster) do
                    local stat = run.playerStats and run.playerStats[fullName] or {}
                    local dps = stat.damage ~= nil and self:GetPlayerDPS(run, stat) or nil
                    local hps = stat.healing ~= nil and self:GetPlayerHPS(run, stat) or nil
                    rows[#rows + 1] = self:CSVRow({
                        run.runId, run.startTime, self:FormatDateTime(run.startTime),
                        run.endTime, self:FormatDateTime(run.endTime), run.ownerCharacter,
                        run.dungeonName, run.mapChallengeModeID, run.mapID, run.season,
                        run.level, self:GetRunResult(run), timedUpgrade, run.timeMS,
                        run.timeLimitSeconds, run.totalDeaths, run.deathPenaltySeconds,
                        run.oldOverallDungeonScore, run.newOverallDungeonScore,
                        run.statsSource, run.statsUnavailable and true or false, run.statsNote,
                        fullName, stat.shortName or self:GetShortName(fullName),
                        stat.classFilename, stat.specID, stat.specName, stat.role,
                        stat.score, stat.previousScore, stat.itemLevel, stat.damage, dps,
                        stat.healing, hps, stat.interrupts, stat.dispels,
                        stat.avoidableDamage, stat.deaths,
                        type(stat.loot) == "table" and table.concat(stat.loot, " | ") or nil,
                    }, columnCount)
                end
            end
        end
    end

    return table.concat(rows, "\n")
end

function MythicTools:ShowCSVExportPopup()
    local csv = self:BuildRawCSVExport()
    local frame = self.csvExportFrame
    if not frame then
        frame = CreateFrame("Frame", "MythicToolsCSVExportFrame", UIParent, "BackdropTemplate")
        frame:SetSize(760, 520)
        frame:SetPoint("CENTER")
        frame:SetFrameStrata("FULLSCREEN_DIALOG")
        frame:SetFrameLevel(500)
        frame:SetClampedToScreen(true)
        frame:EnableMouse(true)
        frame:SetMovable(true)
        frame:RegisterForDrag("LeftButton")
        frame:SetScript("OnDragStart", function(self) self:StartMoving() end)
        frame:SetScript("OnDragStop", function(self) self:StopMovingOrSizing() end)
        frame:Hide()
        self:ApplySurface(frame, COLORS.frameBG, COLORS.frameBorder, COLORS.accent)
        table.insert(UISpecialFrames, frame:GetName())

        local title = CreateFont(frame, 16, COLORS.text)
        title:SetPoint("TOPLEFT", 16, -14)
        title:SetText("Raw CSV Export")

        local hint = CreateFont(frame, 11, COLORS.muted, "RIGHT")
        hint:SetPoint("TOPRIGHT", -54, -17)
        hint:SetText("Click the text, then Ctrl+C.")

        local closeButton = CreateActionButton(frame, 28, 24, "X")
        closeButton:SetPoint("TOPRIGHT", -14, -12)
        closeButton:SetScript("OnClick", function()
            frame:Hide()
        end)

        local box = CreateFrame("Frame", nil, frame, "BackdropTemplate")
        box:SetPoint("TOPLEFT", 16, -48)
        box:SetPoint("BOTTOMRIGHT", -16, 52)
        box:SetBackdrop({bgFile = WHITE_TEXTURE, edgeFile = WHITE_TEXTURE, edgeSize = 1, insets = {left=1, right=1, top=1, bottom=1}})
        box:SetBackdropColor(COLORS.rowBG[1], COLORS.rowBG[2], COLORS.rowBG[3], 1)
        box:SetBackdropBorderColor(0, 0, 0, 1)
        ClipChildren(box)

        local scroll = CreateFrame("ScrollFrame", nil, box, "UIPanelScrollFrameTemplate")
        scroll:SetPoint("TOPLEFT", 8, -8)
        scroll:SetPoint("BOTTOMRIGHT", -28, 8)
        ApplyThinScrollBar(scroll)

        local editBox = CreateFrame("EditBox", nil, scroll)
        editBox:SetMultiLine(true)
        editBox:SetAutoFocus(false)
        editBox:SetMaxLetters(0)
        editBox:SetFontObject(ChatFontNormal)
        editBox:SetTextColor(COLORS.text[1], COLORS.text[2], COLORS.text[3], 1)
        editBox:SetJustifyH("LEFT")
        editBox:SetJustifyV("TOP")
        editBox:SetWidth(700)
        editBox:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
        editBox:SetScript("OnEditFocusGained", function(self) self:HighlightText() end)
        scroll:SetScrollChild(editBox)

        local selectButton = CreateActionButton(frame, 120, 28, "Select all")
        selectButton:SetPoint("BOTTOMLEFT", 16, 14)
        selectButton:SetScript("OnClick", function()
            frame.EditBox:SetFocus()
            frame.EditBox:HighlightText()
        end)

        local doneButton = CreateActionButton(frame, 86, 28, "Done")
        doneButton:SetPoint("BOTTOMRIGHT", -16, 14)
        doneButton:SetScript("OnClick", function()
            frame:Hide()
        end)

        frame.EditBox = editBox
        self.csvExportFrame = frame
    end

    frame.EditBox:SetText(csv)
    frame.EditBox:SetCursorPosition(0)
    frame.EditBox:SetFocus()
    frame.EditBox:HighlightText()
    frame:Show()
    frame:Raise()
end

function MythicTools:ShowTab(tabName, preserveFilters)
    self:BuildUI()
    if not preserveFilters then
        self:ResetNavigationFilters()
        self:SyncFilterWidgets()
    end
    self.db.ui.activeTab = tabName
    for name, page in pairs(self.ui.pages) do
        page:SetShown(name == tabName)
    end
    for name, button in pairs(self.ui.tabButtons) do
        StyleTabButton(button, name == tabName)
    end
    self:RefreshUI()
end

function MythicTools:ToggleMainFrame(forceShow)
    self:BuildUI()
    if forceShow or not self.ui.frame:IsShown() then
        self:ResetNavigationFilters()
        self:SyncFilterWidgets()
        self.ui.frame:Show()
        self:ShowTab(self.db.ui.activeTab or "Runs", true)
    else
        self:HideCompletionPopup()
        self.ui.frame:Hide()
    end
end

function MythicTools:ShowPlayersIndex()
    self.db.ui.playersView = "index"
    self.db.ui.selectedPlayer = nil
    self:BuildUI()
    self:ToggleMainFrame(true)
    self:ShowTab("Players")
end

function MythicTools:ShowRaiderIOCopyPopup(playerName)
    local url = BuildRaiderIOCharacterURL(playerName)
    if not url then
        self:Print("Raider.IO link unavailable for this player.")
        return
    end

    if not StaticPopupDialogs.MYTHICTOOLS_COPY_RAIDERIO then
        StaticPopupDialogs.MYTHICTOOLS_COPY_RAIDERIO = {
            text = "Copy Raider.IO link\nPress Ctrl+C to copy.",
            button1 = OKAY or "OK",
            hasEditBox = true,
            editBoxWidth = 360,
            timeout = 0,
            whileDead = true,
            hideOnEscape = true,
            preferredIndex = 3,
            OnShow = function(dialog, data)
                local editBox = dialog.editBox or dialog.EditBox
                if editBox then
                    editBox:SetText(data or dialog.data or "")
                    editBox:SetFocus()
                    editBox:HighlightText()
                end
            end,
            EditBoxOnEscapePressed = function(editBox)
                editBox:GetParent():Hide()
            end,
            EditBoxOnEnterPressed = function(editBox)
                editBox:HighlightText()
            end,
            OnAccept = function(dialog)
                local editBox = dialog.editBox or dialog.EditBox
                if editBox then
                    editBox:ClearFocus()
                end
            end,
        }
    end

    local dialog = StaticPopup_Show("MYTHICTOOLS_COPY_RAIDERIO", nil, nil, url)
    local editBox = dialog and (dialog.editBox or dialog.EditBox) or nil
    if editBox then
        editBox:SetText(url)
        editBox:SetFocus()
        editBox:HighlightText()
    end
end

function MythicTools:ShowPlayerNoteEditor(playerName)
    playerName = self:NormalizePlayerName(playerName) or self:TrimText(playerName)
    if not playerName or playerName == "" then
        return
    end

    if not StaticPopupDialogs.MYTHICTOOLS_EDIT_PLAYER_NOTE then
        StaticPopupDialogs.MYTHICTOOLS_EDIT_PLAYER_NOTE = {
            text = "Player note",
            button1 = SAVE or "Save",
            button2 = CANCEL or "Cancel",
            hasEditBox = true,
            editBoxWidth = 360,
            timeout = 0,
            whileDead = true,
            hideOnEscape = true,
            preferredIndex = 3,
            OnShow = function(dialog, data)
                local editBox = dialog.editBox or dialog.EditBox
                if editBox then
                    editBox:SetMaxLetters(160)
                    editBox:SetText(MythicTools:GetPlayerNote(data or dialog.playerName or dialog.data) or "")
                    editBox:SetFocus()
                    editBox:HighlightText()
                end
            end,
            OnAccept = function(dialog, data)
                local editBox = dialog.editBox or dialog.EditBox
                local targetPlayer = data or dialog.playerName or dialog.data
                MythicTools:SetPlayerNote(targetPlayer, editBox and editBox:GetText() or "")
                if editBox then
                    editBox:ClearFocus()
                end
                local playerEntry = MythicTools:GetSelectedPlayerEntry()
                if playerEntry and playerEntry.fullName == targetPlayer then
                    MythicTools:RefreshPlayerDetailView(playerEntry)
                end
            end,
            EditBoxOnEscapePressed = function(editBox)
                editBox:GetParent():Hide()
            end,
            EditBoxOnEnterPressed = function(editBox)
                local dialog = editBox:GetParent()
                if dialog and dialog.button1 then
                    dialog.button1:Click()
                end
            end,
        }
    end

    local dialog = StaticPopup_Show("MYTHICTOOLS_EDIT_PLAYER_NOTE", nil, nil, playerName)
    if dialog then
        dialog.playerName = playerName
        local editBox = dialog.editBox or dialog.EditBox
        if editBox then
            editBox:SetMaxLetters(160)
            editBox:SetText(self:GetPlayerNote(playerName) or "")
            editBox:SetFocus()
            editBox:HighlightText()
        end
    end
end

function MythicTools:FocusSelectedPlayerNoteEditor()
    local playerName = self.db and self.db.ui and self.db.ui.selectedPlayer
    if playerName then
        self:ShowPlayerNoteEditor(playerName)
    end
end

function MythicTools:OpenPlayerHistory(playerName, focusNote)
    playerName = self:NormalizePlayerName(playerName) or self:TrimText(playerName)
    if not playerName or playerName == "" then
        return
    end

    self.db.ui.selectedPlayer = playerName
    self.db.ui.playersView = "detail"
    self:BuildUI()
    self.ui.frame:Show()
    self:ShowTab("Players", true)
    if self.ui and self.ui.playerDetailScrollFrame then
        self.ui.playerDetailScrollFrame:SetVerticalScroll(0)
    end
    if self.ui and self.ui.playerDetailScrollBar then
        self.ui.playerDetailScrollBar:SetValue(0)
    end

    if focusNote then
        self.runtime = self.runtime or {}
        self.runtime.pendingNoteFocusPlayer = playerName
        C_Timer.After(0, function()
            if MythicTools.db and MythicTools.db.ui and MythicTools.db.ui.selectedPlayer == playerName then
                MythicTools:FocusSelectedPlayerNoteEditor()
            end
        end)
    end
end

function MythicTools:EditPlayerNote(playerName)
    self:OpenPlayerHistory(playerName, true)
end

function MythicTools:ShowPlayerContextMenu(playerName)
    playerName = self:NormalizePlayerName(playerName) or self:TrimText(playerName)
    if not playerName or playerName == "" then
        return
    end

    local shortName = self:GetShortName(playerName)
    local menu = {
        {
            text = shortName,
            isTitle = true,
            notCheckable = true,
        },
        {
            text = "View player history",
            notCheckable = true,
            func = function()
                MythicTools:OpenPlayerHistory(playerName)
            end,
        },
        {
            text = "Edit note",
            notCheckable = true,
            func = function()
                MythicTools:EditPlayerNote(playerName)
            end,
        },
        {
            text = "Copy Raider.IO link",
            notCheckable = true,
            func = function()
                MythicTools:ShowRaiderIOCopyPopup(playerName)
            end,
        },
    }

    self.playerContextMenuFrame = self.playerContextMenuFrame or CreateFrame("Frame", "MythicToolsPlayerContextMenu", UIParent, "UIDropDownMenuTemplate")
    self.playerContextMenuFrame:SetFrameStrata("FULLSCREEN_DIALOG")
    EasyMenu(menu, self.playerContextMenuFrame, "cursor", 0, 0, "MENU")
end

function MythicTools:SaveSelectedPlayerNote()
    local playerName = self.db and self.db.ui and self.db.ui.selectedPlayer
    if not playerName then
        return
    end

    self:ShowPlayerNoteEditor(playerName)
end

function MythicTools:GetPlayerNameFromMenuContext(contextData)
    if not contextData then
        return nil
    end

    local unit = contextData.unit
    if unit then
        local ok, fullName = pcall(function()
            if UnitExists(unit) then
                return self:GetUnitFullName(unit)
            end
        end)
        if ok and fullName then
            return fullName
        end
    end

    local accountInfo = contextData.accountInfo
    if accountInfo and accountInfo.gameAccountInfo then
        local gameAccountInfo = accountInfo.gameAccountInfo
        return self:NormalizePlayerName(gameAccountInfo.characterName, gameAccountInfo.realmName)
    end

    if contextData.name then
        return self:NormalizePlayerName(contextData.name, contextData.server)
    end

    return nil
end

function MythicTools:GetUnitMenuSummaryText(playerName)
    local playerEntry = self.db and self.db.playersIndex and self.db.playersIndex[playerName]
    if not playerEntry then
        return nil
    end

    return ("%d of %d timed"):format(playerEntry.timedRuns or 0, playerEntry.totalRuns or 0)
end

function MythicTools:TooltipAlreadyHasHistoryLine(tooltip)
    return tooltip and tooltip._smhHistoryTooltipAdded and true or false
end

function MythicTools:NormalizeTooltipPlayerName(name, realm)
    if not self:CanAccessValue(name) or type(name) ~= "string" or name == "" then
        return nil
    end

    return self:NormalizePlayerName(name, realm)
end

function MythicTools:GetPlayerNameFromLFGTooltipOwner(tooltip)
    local owner = tooltip and tooltip.GetOwner and tooltip:GetOwner() or nil
    if type(owner) ~= "table" then
        return nil
    end

    local directName = self:NormalizeTooltipPlayerName(owner.name or owner.memberName or owner.playerName or owner.fullName, owner.realmName or owner.realm)
    if directName then
        return directName
    end

    local resultID = owner.resultID or owner.searchResultID or owner.id
    local memberIndex = owner.memberIdx or owner.memberIndex or owner.memberID or owner.index
    if C_LFGList and C_LFGList.GetSearchResultPlayerInfo and resultID and memberIndex then
        local ok, infoOrName, realmName = pcall(C_LFGList.GetSearchResultPlayerInfo, resultID, memberIndex)
        if ok then
            if type(infoOrName) == "table" then
                local name = self:NormalizeTooltipPlayerName(infoOrName.name or infoOrName.playerName or infoOrName.fullName, infoOrName.realmName or infoOrName.realm)
                if name then
                    return name
                end
            else
                local name = self:NormalizeTooltipPlayerName(infoOrName, realmName)
                if name then
                    return name
                end
            end
        end
    end

    local applicantID = owner.applicantID or owner.applicantId or owner.id
    if C_LFGList and C_LFGList.GetApplicantMemberInfo and applicantID and memberIndex then
        local ok, infoOrName, realmName = pcall(C_LFGList.GetApplicantMemberInfo, applicantID, memberIndex)
        if ok then
            if type(infoOrName) == "table" then
                local name = self:NormalizeTooltipPlayerName(infoOrName.name or infoOrName.playerName or infoOrName.fullName, infoOrName.realmName or infoOrName.realm)
                if name then
                    return name
                end
            else
                local name = self:NormalizeTooltipPlayerName(infoOrName, realmName)
                if name then
                    return name
                end
            end
        end
    end

    return nil
end

function MythicTools:GetPlayerNameFromFriendObject(object)
    if type(object) ~= "table" then
        return nil
    end

    local data = type(object.elementData) == "table" and object.elementData or object
    local directName = self:NormalizeTooltipPlayerName(data.name or data.characterName or data.playerName or data.fullName, data.realmName or data.realm)
    if directName then
        return directName
    end

    local gameAccountInfo = data.gameAccountInfo
    if type(gameAccountInfo) == "table" and gameAccountInfo.characterName then
        return self:NormalizeTooltipPlayerName(gameAccountInfo.characterName, gameAccountInfo.realmName)
    end

    local buttonType = data.buttonType or data.type
    local buttonID = data.id or data.index or data.friendIndex
    local isWowFriend = buttonType == "FRIEND" or buttonType == "WOW" or buttonType == FRIENDS_BUTTON_TYPE_WOW or buttonType == 1
    local isBattleNetFriend = buttonType == "BN_FRIEND" or buttonType == "BNET" or buttonType == FRIENDS_BUTTON_TYPE_BNET or buttonType == 2
    if isWowFriend and buttonID then
        if C_FriendList and C_FriendList.GetFriendInfoByIndex then
            local ok, info = pcall(C_FriendList.GetFriendInfoByIndex, buttonID)
            if ok and type(info) == "table" and info.name then
                local name = self:NormalizeTooltipPlayerName(info.name)
                if name then
                    return name
                end
            end
        end
        if GetFriendInfo then
            local ok, name = pcall(GetFriendInfo, buttonID)
            if ok then
                local normalized = self:NormalizeTooltipPlayerName(name)
                if normalized then
                    return normalized
                end
            end
        end
    end

    if isBattleNetFriend and buttonID then
        if C_BattleNet and C_BattleNet.GetFriendAccountInfo then
            local ok, accountInfo = pcall(C_BattleNet.GetFriendAccountInfo, buttonID)
            local info = ok and accountInfo and accountInfo.gameAccountInfo
            if type(info) == "table" and info.characterName and (not BNET_CLIENT_WOW or info.clientProgram == BNET_CLIENT_WOW or info.clientProgram == "WoW") then
                local name = self:NormalizeTooltipPlayerName(info.characterName, info.realmName)
                if name then
                    return name
                end
            end
        end
        if BNGetFriendInfo then
            local ok, _, _, _, _, _, client, _, _, _, _, _, _, _, _, _, gameAccountInfo = pcall(BNGetFriendInfo, buttonID)
            if ok and client == BNET_CLIENT_WOW and type(gameAccountInfo) == "table" and gameAccountInfo.characterName then
                local name = self:NormalizeTooltipPlayerName(gameAccountInfo.characterName, gameAccountInfo.realmName)
                if name then
                    return name
                end
            end
        end
    end

    return nil
end

function MythicTools:GetPlayerNameFromFriendTooltipOwner(tooltip)
    local owner = tooltip and tooltip.GetOwner and tooltip:GetOwner() or nil
    return self:GetPlayerNameFromFriendObject(owner)
end

function MythicTools:GetPlayerNameFromTooltip(tooltip, tooltipData)
    local guid = tooltipData and tooltipData.guid
    if guid and C_PlayerInfo and C_PlayerInfo.GetPlayerInfoByGUID then
        local playerInfo = C_PlayerInfo.GetPlayerInfoByGUID(guid)
        if playerInfo and playerInfo.name then
            return self:NormalizePlayerName(playerInfo.name, playerInfo.realmName)
        end
    end

    local lfgPlayerName = self:GetPlayerNameFromLFGTooltipOwner(tooltip)
    if lfgPlayerName then
        return lfgPlayerName
    end

    local friendPlayerName = self:GetPlayerNameFromFriendTooltipOwner(tooltip)
    if friendPlayerName then
        return friendPlayerName
    end

    if tooltip and tooltip.GetUnit then
        -- Blizzard can expose protected unit tokens on some tooltips.
        -- Resolve them defensively so bad tokens simply produce no result.
        local ok, fullName = pcall(function()
            local _, unit = tooltip:GetUnit()
            if unit and UnitExists(unit) and UnitIsPlayer(unit) then
                return self:GetUnitFullName(unit)
            end
        end)
        if ok and fullName then
            return fullName
        end
    end

    return nil
end

function MythicTools:AddHistoryToBlizzardTooltip(tooltip, tooltipData)
    if not (tooltip and tooltip.AddLine) or self:TooltipAlreadyHasHistoryLine(tooltip) then
        return
    end

    local playerName = self:GetPlayerNameFromTooltip(tooltip, tooltipData)
    local summary = self:GetPlayerTooltipSummary(playerName)
    if not summary then
        return
    end

    self:AddPlayerHistoryTooltipLines(tooltip, summary)
    tooltip._smhHistoryTooltipAdded = true
    tooltip:Show()
end

function MythicTools:AddHistoryToTooltipForPlayer(tooltip, playerName)
    if not (tooltip and tooltip.AddLine) or self:TooltipAlreadyHasHistoryLine(tooltip) then
        return false
    end

    local summary = self:GetPlayerTooltipSummary(playerName)
    if not summary then
        return false
    end

    self:AddPlayerHistoryTooltipLines(tooltip, summary)
    tooltip._smhHistoryTooltipAdded = true
    tooltip:Show()
    return true
end

function MythicTools:AddHistoryToLFGSearchTooltip(tooltip, resultID)
    if not (C_LFGList and C_LFGList.GetSearchResultInfo and resultID) then
        return
    end

    local searchResultInfo = C_LFGList.GetSearchResultInfo(resultID)
    local leaderName = searchResultInfo and searchResultInfo.leaderName
    if not self:CanAccessValue(leaderName) or type(leaderName) ~= "string" or leaderName == "" then
        return
    end

    tooltip = tooltip or GameTooltip
    if C_Timer then
        C_Timer.After(0, function()
            MythicTools:AddHistoryToTooltipForPlayer(tooltip, leaderName)
        end)
    else
        self:AddHistoryToTooltipForPlayer(tooltip, leaderName)
    end
end

function MythicTools:AddHistoryToLFGApplicantTooltip(button)
    if not (button and C_LFGList and C_LFGList.GetApplicantMemberInfo) then
        return
    end

    local parent = button
    local applicantID = button.applicantID
    while not applicantID and parent and parent.GetParent do
        parent = parent:GetParent()
        applicantID = parent and parent.applicantID
    end

    local memberIndex = button.memberIdx or button.memberIndex
    if not (applicantID and memberIndex) then
        return
    end

    local ok, fullName = pcall(C_LFGList.GetApplicantMemberInfo, applicantID, memberIndex)
    if not ok or not self:CanAccessValue(fullName) or type(fullName) ~= "string" or fullName == "" then
        return
    end

    if GameTooltip and not GameTooltip:GetOwner() then
        GameTooltip:SetOwner(button, "ANCHOR_RIGHT")
    end
    self:AddHistoryToTooltipForPlayer(GameTooltip, fullName)
end

function MythicTools:HookLFGTooltipButton(button)
    if not (button and button.HookScript) or (self.lfgTooltipHookedButtons and self.lfgTooltipHookedButtons[button]) then
        return
    end

    if button.Members then
        for _, memberButton in pairs(button.Members) do
            self:HookLFGTooltipButton(memberButton)
        end
    end

    self.lfgTooltipHookedButtons = self.lfgTooltipHookedButtons or {}
    self.lfgTooltipHookedButtons[button] = true
    button:HookScript("OnEnter", function(frame)
        if frame.applicantID and frame.Members then
            for _, memberButton in pairs(frame.Members) do
                MythicTools:HookLFGTooltipButton(memberButton)
            end
            return
        end

        C_Timer.After(0, function()
            MythicTools:AddHistoryToLFGApplicantTooltip(frame)
        end)
    end)
end

function MythicTools:HookLFGTooltipButtons(buttons)
    for _, button in pairs(buttons or {}) do
        self:HookLFGTooltipButton(button)
    end
end

function MythicTools:HookVisibleLFGTooltipButtons(scrollBox)
    if not scrollBox then
        return
    end

    if scrollBox.GetFrames then
        local ok, frames = pcall(scrollBox.GetFrames, scrollBox)
        if ok and type(frames) == "table" then
            self:HookLFGTooltipButtons(frames)
        end
    end

    if scrollBox.ForEachFrame then
        pcall(scrollBox.ForEachFrame, scrollBox, function(button)
            MythicTools:HookLFGTooltipButton(button)
        end)
    end
end

function MythicTools:AddHistoryToFriendTooltip(button)
    local fullName = self:GetPlayerNameFromFriendObject(button)
    if not fullName then
        return
    end

    if FriendsTooltip and FriendsTooltip:IsShown() and GameTooltip then
        local xOffset = FriendsTooltip.GetWidth and -FriendsTooltip:GetWidth() or 0
        local ownerSet, ownerExisted, ownerSetSame = self:SetTooltipOwnerSafely(GameTooltip, FriendsTooltip, "ANCHOR_BOTTOMRIGHT", xOffset, -4)
        if self:AddHistoryToTooltipForPlayer(GameTooltip, fullName) then
            return
        end
        if ownerSet and not ownerExisted then
            GameTooltip:Hide()
        end
        return
    end

    self:SetTooltipOwnerSafely(GameTooltip, button, "ANCHOR_RIGHT", 0, 0)
    self:AddHistoryToTooltipForPlayer(GameTooltip, fullName)
end

function MythicTools:AddHistoryToFriendsTooltip()
    local button = FriendsTooltip and FriendsTooltip.button
    if button then
        self:AddHistoryToFriendTooltip(button)
    end
end

function MythicTools:HideFriendHistoryTooltip()
    if GameTooltip then
        GameTooltip:Hide()
    end
end

function MythicTools:HookFriendTooltipButton(button)
    if not (button and button.HookScript) or (self.friendTooltipHookedButtons and self.friendTooltipHookedButtons[button]) then
        return
    end

    self.friendTooltipHookedButtons = self.friendTooltipHookedButtons or {}
    self.friendTooltipHookedButtons[button] = true
    button:HookScript("OnEnter", function(frame)
        C_Timer.After(0, function()
            MythicTools:AddHistoryToFriendTooltip(frame)
        end)
    end)
end

function MythicTools:HookVisibleFriendTooltipButtons()
    if FriendsFrameFriendsScrollFrame and FriendsFrameFriendsScrollFrame.buttons then
        for _, button in pairs(FriendsFrameFriendsScrollFrame.buttons) do
            self:HookFriendTooltipButton(button)
        end
    end

    if FriendsListFrame and FriendsListFrame.ScrollBox then
        if FriendsListFrame.ScrollBox.GetFrames then
            local ok, frames = pcall(FriendsListFrame.ScrollBox.GetFrames, FriendsListFrame.ScrollBox)
            if ok and type(frames) == "table" then
                for _, button in pairs(frames) do
                    self:HookFriendTooltipButton(button)
                end
            end
        end
        if FriendsListFrame.ScrollBox.ForEachFrame then
            pcall(FriendsListFrame.ScrollBox.ForEachFrame, FriendsListFrame.ScrollBox, function(button)
                MythicTools:HookFriendTooltipButton(button)
            end)
        end
    end
end

function MythicTools:InitializeFriendTooltips()
    if self.friendTooltipsInitialized then
        return
    end

    if not FriendsFrame and C_AddOns and C_AddOns.LoadAddOn then
        pcall(C_AddOns.LoadAddOn, "Blizzard_Friends")
    end

    if not FriendsFrame then
        self.friendTooltipHookAttempts = (self.friendTooltipHookAttempts or 0) + 1
        if self.friendTooltipHookAttempts <= 20 and C_Timer then
            C_Timer.After(1, function()
                MythicTools:InitializeFriendTooltips()
            end)
        end
        return
    end

    if not FriendsTooltip then
        self.friendTooltipHookAttempts = (self.friendTooltipHookAttempts or 0) + 1
        if self.friendTooltipHookAttempts <= 20 and C_Timer then
            C_Timer.After(1, function()
                MythicTools:InitializeFriendTooltips()
            end)
        end
        return
    end

    self:HookVisibleFriendTooltipButtons()
    if FriendsFrame.HookScript then
        FriendsFrame:HookScript("OnShow", function()
            MythicTools:HookVisibleFriendTooltipButtons()
        end)
    end
    if FriendsFrameFriendsScrollFrame and FriendsFrameFriendsScrollFrame.HookScript then
        FriendsFrameFriendsScrollFrame:HookScript("OnShow", function()
            MythicTools:HookVisibleFriendTooltipButtons()
        end)
    end
    if hooksecurefunc and not self.friendTooltipShowHooked then
        hooksecurefunc(FriendsTooltip, "Show", function()
            if C_Timer then
                C_Timer.After(0, function()
                    MythicTools:AddHistoryToFriendsTooltip()
                end)
            else
                MythicTools:AddHistoryToFriendsTooltip()
            end
        end)
        hooksecurefunc(FriendsTooltip, "Hide", function()
            MythicTools:HideFriendHistoryTooltip()
        end)
        self.friendTooltipShowHooked = true
    end
    self.friendTooltipsInitialized = true
end

function MythicTools:InitializeLFGTooltips()
    if self.lfgTooltipsInitialized then
        return
    end

    if not self.lfgTooltipWatcher and CreateFrame then
        self.lfgTooltipWatcher = CreateFrame("Frame")
        self.lfgTooltipWatcher:SetScript("OnEvent", function()
            MythicTools:InitializeLFGTooltips()
        end)
        self.lfgTooltipWatcher:RegisterEvent("ADDON_LOADED")
        self.lfgTooltipWatcher:RegisterEvent("LFG_LIST_SEARCH_RESULTS_RECEIVED")
        self.lfgTooltipWatcher:RegisterEvent("LFG_LIST_APPLICANT_LIST_UPDATED")
    end

    if not LFGListFrame and C_AddOns and C_AddOns.LoadAddOn then
        pcall(C_AddOns.LoadAddOn, "Blizzard_GroupFinder")
    end

    if not (LFGListFrame and LFGListFrame.SearchPanel and LFGListFrame.ApplicationViewer) then
        self.lfgTooltipHookAttempts = (self.lfgTooltipHookAttempts or 0) + 1
        if self.lfgTooltipHookAttempts <= 20 and C_Timer then
            C_Timer.After(1, function()
                MythicTools:InitializeLFGTooltips()
            end)
        end
        return
    end

    if not LFGListUtil_SetSearchEntryTooltip then
        self.lfgTooltipHookAttempts = (self.lfgTooltipHookAttempts or 0) + 1
        if self.lfgTooltipHookAttempts <= 20 and C_Timer then
            C_Timer.After(1, function()
                MythicTools:InitializeLFGTooltips()
            end)
        end
        return
    end

    if hooksecurefunc and not self.lfgSearchTooltipHooked then
        hooksecurefunc("LFGListUtil_SetSearchEntryTooltip", function(tooltip, resultID)
            MythicTools:AddHistoryToLFGSearchTooltip(tooltip, resultID)
        end)
        self.lfgSearchTooltipHooked = true
    end

    if ScrollBoxUtil and ScrollBoxUtil.OnViewFramesChanged then
        ScrollBoxUtil:OnViewFramesChanged(LFGListFrame.SearchPanel.ScrollBox, function(buttons)
            MythicTools:HookLFGTooltipButtons(buttons)
        end)
        ScrollBoxUtil:OnViewFramesChanged(LFGListFrame.ApplicationViewer.ScrollBox, function(buttons)
            MythicTools:HookLFGTooltipButtons(buttons)
        end)
    end
    self:HookVisibleLFGTooltipButtons(LFGListFrame.SearchPanel.ScrollBox)
    self:HookVisibleLFGTooltipButtons(LFGListFrame.ApplicationViewer.ScrollBox)

    if LFGListFrame.SearchPanel.HookScript then
        LFGListFrame.SearchPanel:HookScript("OnShow", function()
            MythicTools:HookVisibleLFGTooltipButtons(LFGListFrame.SearchPanel.ScrollBox)
        end)
    end
    if LFGListFrame.ApplicationViewer.HookScript then
        LFGListFrame.ApplicationViewer:HookScript("OnShow", function()
            MythicTools:HookVisibleLFGTooltipButtons(LFGListFrame.ApplicationViewer.ScrollBox)
        end)
    end
    if self.lfgTooltipWatcher then
        self.lfgTooltipWatcher:UnregisterAllEvents()
    end
    self.lfgTooltipsInitialized = true
end

function MythicTools:InitializeBlizzardUnitTooltip()
    if self.blizzardUnitTooltipInitialized then
        return
    end

    if TooltipDataProcessor and TooltipDataProcessor.AddTooltipPostCall and Enum and Enum.TooltipDataType and Enum.TooltipDataType.Unit then
        TooltipDataProcessor.AddTooltipPostCall(Enum.TooltipDataType.Unit, function(tooltip, tooltipData)
            MythicTools:AddHistoryToBlizzardTooltip(tooltip, tooltipData)
        end)
    end

    if GameTooltip and GameTooltip.HookScript then
        if GameTooltip.HasScript and GameTooltip:HasScript("OnTooltipSetUnit") then
            GameTooltip:HookScript("OnTooltipSetUnit", function(tooltip)
                MythicTools:AddHistoryToBlizzardTooltip(tooltip)
            end)
        end
        GameTooltip:HookScript("OnShow", function(tooltip)
            MythicTools:AddHistoryToBlizzardTooltip(tooltip)
        end)
        GameTooltip:HookScript("OnTooltipCleared", function(tooltip)
            tooltip._smhHistoryTooltipAdded = nil
        end)
        GameTooltip:HookScript("OnHide", function(tooltip)
            tooltip._smhHistoryTooltipAdded = nil
        end)
    else
        return
    end

    self:InitializeLFGTooltips()
    self:InitializeFriendTooltips()
    self.blizzardUnitTooltipInitialized = true
end

function MythicTools:HandleUnitPopupMenu(owner, rootDescription, contextData)
    local playerName = self:GetPlayerNameFromMenuContext(contextData)
    if not playerName or not self:HasPlayerHistory(playerName) then
        return
    end

    local summaryText = self:GetUnitMenuSummaryText(playerName)
    if not summaryText then
        return
    end

    rootDescription:CreateDivider()
    rootDescription:CreateTitle(ADDON_TITLE_TEXT)
    rootDescription:CreateButton(summaryText, function()
        MythicTools:OpenPlayerHistory(playerName)
    end)
end

function MythicTools:InitializeUnitPopupMenu()
    if self.unitPopupMenuInitialized then
        return
    end

    local modifyMenu = Menu and Menu.ModifyMenu
    if not modifyMenu then
        return
    end

    local supportedMenus = {
        "MENU_UNIT_PARTY",
        "MENU_UNIT_PLAYER",
        "MENU_UNIT_RAID",
        "MENU_UNIT_RAID_PLAYER",
        "MENU_UNIT_TARGET",
        "MENU_UNIT_FOCUS",
        "MENU_UNIT_SELF",
        "MENU_UNIT_FRIEND",
        "MENU_UNIT_GUILD",
        "MENU_UNIT_GUILD_OFFLINE",
    }

    for _, tag in ipairs(supportedMenus) do
        modifyMenu(tag, function(owner, rootDescription, contextData)
            MythicTools:HandleUnitPopupMenu(owner, rootDescription, contextData)
        end)
    end

    self.unitPopupMenuInitialized = true
end
function MythicTools:RefreshUI()
    if not self.ui then
        return
    end

    if self.ui.sidebarRuns then
        self.ui.sidebarRuns.Value:SetText(tostring(#(self.db.runs or {})))
    end
    if self.ui.sidebarPlayers then
        self.ui.sidebarPlayers.Value:SetText(tostring(self:GetPlayerCount()))
    end
    if self.ui.sidebarStatus then
        if self.runtime and self.runtime.activeRun then
            self.ui.sidebarStatus.Value:SetText(("+%d"):format(self.runtime.activeRun.keyLevel or 0))
            self.ui.sidebarStatus.Label:SetText(self.runtime.activeRun.dungeonName or "Active run")
        else
            self.ui.sidebarStatus.Value:SetText("--")
            self.ui.sidebarStatus.Label:SetText("No active run")
        end
    end

    self:RefreshOwnedCharacterFilterOptions()
    self:RefreshRunView()
    self:RefreshPlayersView()
    self:RefreshSettingsView()
    self:RefreshStatsView()
end

function MythicTools:RefreshRunView()
    if not self.ui then
        return
    end

    local runs = self:GetFilteredRuns()
    self.ui.filteredRuns = runs
    self.ui.runCountLabel:SetText(("%d runs"):format(#runs))

    local hasVisibleSelection = false
    for _, run in ipairs(runs) do
        if run.runId == self.db.ui.selectedRunId then
            hasVisibleSelection = true
            break
        end
    end
    if not hasVisibleSelection then
        self.db.ui.selectedRunId = runs[1] and runs[1].runId or nil
    end

    local showDetail = false
    if self.ui.runsFilterBar then
        self.ui.runsFilterBar:SetShown(not showDetail)
    end
    if self.ui.runsListView then
        self.ui.runsListView:SetShown(not showDetail)
    end
    if self.ui.runsDetailView then
        self.ui.runsDetailView:SetShown(showDetail)
    end

    FauxScrollFrame_Update(self.ui.runsScroll, #runs, RUN_ROWS, RUN_ROW_HEIGHT)
    local offset = FauxScrollFrame_GetOffset(self.ui.runsScroll)

    for rowIndex = 1, RUN_ROWS do
        local row = self.ui.runRows[rowIndex]
        local run = runs[offset + rowIndex]
        if run then
            row:Show()
            row.runId = run.runId
            self:SetRunArt(row.Icon, nil, run)
            row.Title:SetText(("%s +%d  •  %s"):format(run.dungeonName or "Unknown", run.level or 0, ColorizeInlineText(GetRunStatusText(run), GetRunStatusColor(run))))
            row.Subtitle:SetText("")
            row.Subtitle:Hide()

            local ownerName, ownerStat = GetRunOwnerInfo(run)
            if ownerName then
                self:SetRoleBadge(row.OwnerRole, ownerStat and ownerStat.role)
                row.OwnerSpec.Icon:SetTexture(self:ResolveDisplayIcon(ownerStat and ownerStat.specIconID, ownerStat and ownerStat.specID, ownerStat and ownerStat.classFilename))
                row.OwnerSpec.Icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
                row.OwnerName:SetText(self:GetShortName(ownerName))
                local classColor = ownerStat and ownerStat.classFilename and RAID_CLASS_COLORS and RAID_CLASS_COLORS[ownerStat.classFilename]
                if classColor then
                    row.OwnerName:SetTextColor(classColor.r, classColor.g, classColor.b, 1)
                else
                    SetTextColor(row.OwnerName, COLORS.muted)
                end
                row.OwnerSpec:Show()
                row.OwnerName:Show()
            else
                row.OwnerRole:Hide()
                row.OwnerSpec:Hide()
                row.OwnerName:SetText("")
                row.OwnerName:Hide()
            end

            row.RightTop:SetText(self:FormatDateTime(run.endTime))
            row.RightBottom:SetText(self:FormatDurationMS(run.timeMS))
            SetTextColor(row.RightTop, COLORS.muted)
            StyleListButton(row, run.runId == self.db.ui.selectedRunId, GetRunStatusColor(run))
        else
            row:Hide()
            row.runId = nil
            if row.OwnerRole then row.OwnerRole:Hide() end
            if row.OwnerSpec then row.OwnerSpec:Hide() end
            if row.OwnerName then row.OwnerName:Hide() end
        end
    end

    if self.ui.runsEmptyState then
        self.ui.runsEmptyState:SetShown(#runs == 0)
    end
end
function MythicTools:RefreshPlayersView()
    if not self.ui then
        return
    end

    local players = self:GetFilteredPlayers()
    self.ui.filteredPlayers = players
    if self.ui.playerCountLabel then
        self.ui.playerCountLabel:SetText(("%d players"):format(#players))
    end

    if self.ui.playersEmptyState then
        self.ui.playersEmptyState:SetShown(#players == 0)
    end

    if self.ui.playersTable then
        local rows = self:BuildPlayerAnalyticsTableData(players)
        self:ApplyPlayersTableSortState()
        self.ui.playersTable:SetData(rows)
        self.ui.playersTable:SortData()
        if self.ui.playersTable.frame then
            self.ui.playersTable.frame:SetShown(#players > 0)
        end
    end

    local playerEntry = self:GetSelectedPlayerEntry()
    local showDetail = self.db.ui.playersView == "detail" and playerEntry ~= nil

    if self.ui.playersIndexView then
        self.ui.playersIndexView:SetShown(not showDetail)
    end
    if self.ui.playersDetailView then
        self.ui.playersDetailView:SetShown(showDetail)
    end

    if showDetail then
        self:RefreshPlayerDetailView(playerEntry)
    elseif self.db.ui.playersView == "detail" then
        self.db.ui.playersView = "index"
        self.db.ui.selectedPlayer = nil
    end
end

function MythicTools:RefreshPlayerDetailView(playerEntry)
    if not (self.ui and self.ui.playersDetailView and playerEntry) then
        return
    end

    local analyticsEntry = self:BuildFilteredPlayerAggregate(playerEntry, nil)
    local visualSpecName = playerEntry.specName or nil
    local visualRole = playerEntry.role and self:GetRoleLabel(playerEntry.role) or nil
    local heroMeta = {}
    if visualSpecName then
        heroMeta[#heroMeta + 1] = visualSpecName
    end
    if visualRole then
        heroMeta[#heroMeta + 1] = visualRole
    end
    if playerEntry.classFilename and LOCALIZED_CLASS_NAMES_MALE and LOCALIZED_CLASS_NAMES_MALE[playerEntry.classFilename] then
        heroMeta[#heroMeta + 1] = LOCALIZED_CLASS_NAMES_MALE[playerEntry.classFilename]
    end

    self:SetPortraitWidget(self.ui.playerDetailPortrait, playerEntry.fullName, playerEntry.classFilename, playerEntry.specIconID, playerEntry.specID)
    self:SetRoleBadge(self.ui.playerDetailRoleBadge, playerEntry.role)
    local characterName, realmName = GetPlayerNameParts(playerEntry.fullName)
    self.ui.playerDetailName:SetText(characterName ~= "" and characterName or (playerEntry.shortName or playerEntry.fullName or "Unknown player"))
    if self.ui.playerDetailRealm then
        self.ui.playerDetailRealm:SetText(realmName ~= "" and realmName or "Unknown realm")
    end
    self.ui.playerDetailMeta:SetText(#heroMeta > 0 and table.concat(heroMeta, "  |  ") or "Historical player summary")

    local raiderIOUrl = BuildRaiderIOCharacterURL(playerEntry.fullName)
    if self.ui.playerDetailRaiderIOButton then
        self.ui.playerDetailRaiderIOButton.playerName = playerEntry.fullName
        if raiderIOUrl then
            self.ui.playerDetailRaiderIOButton:Enable()
        else
            self.ui.playerDetailRaiderIOButton:Disable()
        end
        self.ui.playerDetailRaiderIOButton.Text:SetText(raiderIOUrl and "Copy Raider.IO" or "No Raider.IO")
        SetTextColor(self.ui.playerDetailRaiderIOButton.Text, raiderIOUrl and COLORS.text or COLORS.subdued)
    end

    if self.ui.playerDetailMetricCards then
        self:SetMetricCardValue(self.ui.playerDetailMetricCards.runs, tostring(analyticsEntry.totalRuns or 0), "Total runs")
        self:SetMetricCardValue(self.ui.playerDetailMetricCards.timed, tostring(analyticsEntry.timedRuns or 0), ("%.1f%% success"):format(analyticsEntry.successRate or 0), COLORS.success)
        self:SetMetricCardValue(self.ui.playerDetailMetricCards.bestKey, FormatKeyLevel(analyticsEntry.bestTimedLevel), "Best timed")
        self:SetMetricCardValue(self.ui.playerDetailMetricCards.avgKey, string.format("%.1f", analyticsEntry.averageLevel or 0), "Average key")
        self:SetMetricCardValue(self.ui.playerDetailMetricCards.maxDps, self:FormatAmount(analyticsEntry.maxDps or 0), "Maximum DPS", COLORS.accentSoft)
        self:SetMetricCardValue(self.ui.playerDetailMetricCards.avgDps, self:FormatAmount(analyticsEntry.averageDps or 0), "Average DPS")
        self:SetMetricCardValue(self.ui.playerDetailMetricCards.maxHps, self:FormatAmount(analyticsEntry.maxHps or 0), "Maximum HPS", COLORS.accentSoft)
        self:SetMetricCardValue(self.ui.playerDetailMetricCards.avgHps, self:FormatAmount(analyticsEntry.averageHps or 0), "Average HPS")
    end

    local allRecentRuns = self:GetPlayerRecentRuns(playerEntry.fullName, nil, nil)
    local latestFilteredRun = allRecentRuns[1]
    local noteParts = {}
    if latestFilteredRun and latestFilteredRun.dungeonName then
        noteParts[#noteParts + 1] = ("Last seen: %s %s"):format(latestFilteredRun.dungeonName, FormatKeyLevel(latestFilteredRun.level))
    end
    if latestFilteredRun and latestFilteredRun.endTime then
        noteParts[#noteParts + 1] = self:FormatDateTime(latestFilteredRun.endTime)
    end
    if latestFilteredRun and latestFilteredRun.result then
        noteParts[#noteParts + 1] = latestFilteredRun.result == "timed" and "Timed" or (latestFilteredRun.result == "abandoned" and "Abandoned" or "Overtime")
    end
    self.ui.playerDetailHeroNote:SetText(#noteParts > 0 and table.concat(noteParts, "  |  ") or "No recent metadata available.")

    local specEntries = self:GetSortedBreakdownEntries(playerEntry.specBreakdown)
    local roleShortLabel = {
        DAMAGER = "DPS",
        HEALER = "Heal",
        TANK = "Tank",
    }
    local visibleChipCount = 0
    for index, chip in ipairs(self.ui.playerDetailSpecChips or {}) do
        local entry = specEntries[index]
        if entry then
            visibleChipCount = visibleChipCount + 1
            chip:Show()
            chip:ClearAllPoints()
            chip:SetPoint("RIGHT", self.ui.playerDetailSpecChipsContainer, "RIGHT", -((visibleChipCount - 1) * 92), 0)
            chip.Icon:SetTexture(self:ResolveDisplayIcon(entry.specIconID, entry.specID, playerEntry.classFilename))
            chip.Icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
            chip.Count:SetText(("x%d"):format(entry.totalRuns or 0))
            chip.Role:SetText(roleShortLabel[entry.role] or "")
            chip.tooltipText = ("%s  |  %d runs"):format(entry.specName or entry.label or "Unknown spec", entry.totalRuns or 0)
            chip:SetScript("OnEnter", function(self)
                GameTooltip:SetOwner(self, "ANCHOR_TOP")
                GameTooltip:AddLine(self.tooltipText or "Unknown spec", COLORS.text[1], COLORS.text[2], COLORS.text[3])
                GameTooltip:Show()
            end)
            chip:SetScript("OnLeave", function()
                GameTooltip:Hide()
            end)
        else
            chip:Hide()
        end
    end
    if self.ui.playerDetailSpecChipsContainer then
        local reserveWidth = visibleChipCount > 0 and (88 + ((visibleChipCount - 1) * 92)) or 0
        self.ui.playerDetailSpecChipsContainer:SetWidth(reserveWidth)
        self.ui.playerDetailSpecChipsContainer:SetShown(visibleChipCount > 0)
    end
    local noteText = self:GetPlayerNote(playerEntry.fullName)
    if self.ui.playerDetailNoteText then
        self.ui.playerDetailNoteText:SetText(noteText ~= "" and noteText or "No note yet.")
        SetTextColor(self.ui.playerDetailNoteText, noteText ~= "" and COLORS.text or COLORS.subdued)
    end
    if self.ui.playerDetailNoteEdit then
        self.ui.playerDetailNoteEdit.playerName = playerEntry.fullName
        self.ui.playerDetailNoteEdit.Text:SetText(noteText ~= "" and "Edit note" or "Add note")
    end
    if self.ui.playerDetailNoteStatus then
        self.ui.playerDetailNoteStatus:SetText("")
        self.ui.playerDetailNoteStatus:Hide()
    end

    local recentRuns = {}
    for i = 1, min(PLAYER_DETAIL_HISTORY_ROWS, #allRecentRuns) do
        recentRuns[i] = allRecentRuns[i]
    end
    if self.ui.playerDetailHistoryCount then
        self.ui.playerDetailHistoryCount:SetText(("%d run%s total"):format(#allRecentRuns, #allRecentRuns == 1 and "" or "s"))
    end
    if self.ui.playerDetailEmptyHistory then
        self.ui.playerDetailEmptyHistory:SetShown(#allRecentRuns == 0)
    end
    if self.ui.playerDetailSeeAllButton then
        self.ui.playerDetailSeeAllButton:SetShown(#allRecentRuns > 0)
    end

    for index, row in ipairs(self.ui.playerDetailHistoryRows or {}) do
        local run = recentRuns[index]
        if run then
            local upgradeText = GetRunUpgradeText(run)
            local scoreText = GetRunScoreText(run)
            local subtitle = ("%s  |  %d deaths  |  %d item%s"):format(
                self:FormatDateTime(run.endTime),
                run.totalDeaths or 0,
                self:GetRunLootCount(run),
                self:GetRunLootCount(run) == 1 and "" or "s"
            )
            if upgradeText ~= "" then
                subtitle = subtitle .. ("  |  Upgrade %s"):format(upgradeText)
            end
            if scoreText ~= "" then
                subtitle = subtitle .. ("  |  %s"):format(scoreText)
            end
            row:Show()
            row.runId = run.runId
            self:SetRunArt(row.Icon, nil, run)
            row.Title:SetText(("%s %s"):format(run.dungeonName or "Unknown", FormatKeyLevel(run.level)))
            row.Subtitle:SetText(subtitle)
            row.RightTop:SetText(run.result == "timed" and "Timed" or (run.result == "abandoned" and "Abandoned" or "Overtime"))
            row.RightBottom:SetText(self:FormatDurationMS(run.timeMS))
            StyleListButton(row, false, GetRunStatusColor(run))
        else
            row.runId = nil
            row:Hide()
        end
    end

    if self.runtime and self.runtime.pendingNoteFocusPlayer == playerEntry.fullName then
        self.runtime.pendingNoteFocusPlayer = nil
        C_Timer.After(0, function()
            MythicTools:FocusSelectedPlayerNoteEditor()
        end)
    end
end

function MythicTools:RefreshSettingsView()
    if not (self.ui
        and self.ui.settingsAlerts
        and self.ui.settingsMinimap
        and self.ui.settingsPopupEnabled
        and self.ui.settingsLootHistory
        and self.ui.settingsPopupTrigger
        and self.ui.settingsMaxRuns
        and self.ui.settingsPopupDelay
        and self.ui.settingsPopupScale) then
        return
    end

    self.ui.settingsAlerts.Check:SetChecked(self.db.settings.chatAlerts)
    self.ui.settingsMinimap.Check:SetChecked(self.db.settings.showMinimapButton)
    self.ui.settingsPopupEnabled.Check:SetChecked(self.db.settings.showCompletionPopup ~= false)
    self.ui.settingsLootHistory.Check:SetChecked(self.db.settings.showLootHistory ~= false)
    self.ui.settingsPopupTrigger:SetValue(self.db.settings.completionPopupTrigger or "LOOT_OPENED")

    if not self.ui.settingsMaxRuns.Input:HasFocus() then
        self.ui.settingsMaxRuns.Input:SetText(tostring(self.db.settings.maxRuns or 500))
    end
    if not self.ui.settingsPopupDelay.Input:HasFocus() then
        self.ui.settingsPopupDelay.Input:SetText(tostring(self.db.settings.completionPopupDelay or 0))
    end
    if not self.ui.settingsPopupScale.Input:HasFocus() then
        self.ui.settingsPopupScale.Input:SetText(string.format("%.1f", self:GetCompletionPopupScale()))
    end

    self:ApplyCompletionPopupScale()

    local activeRunText = "No active run"
    if self.runtime and self.runtime.activeRun then
        activeRunText = ("Active run: %s +%d"):format(self.runtime.activeRun.dungeonName or "Unknown", self.runtime.activeRun.keyLevel or 0)
    end

    self.ui.settingsSummary:SetText((
        "Saved runs: %d\nIndexed players: %d\n%s\nPopup: %s (%s, delay %ss, scale %.1f)\nData source: Blizzard damage meter"
    ):format(
        #self.db.runs,
        self:GetPlayerCount(),
        activeRunText,
        self.db.settings.showCompletionPopup == false and "disabled" or "enabled",
        GetPopupTriggerLabel(self.db.settings.completionPopupTrigger or "LOOT_OPENED"),
        tostring(self.db.settings.completionPopupDelay or 0),
        self:GetCompletionPopupScale()
    ))
end
local function FormatTotalTime(ms)
    if not ms or ms <= 0 then return "--" end
    local s = floor(ms / 1000)
    local d = floor(s / 86400)
    local h = floor((s % 86400) / 3600)
    local m = floor((s % 3600) / 60)
    if d > 0 then return ("%dd %dh"):format(d, h)
    elseif h > 0 then return ("%dh %dm"):format(h, m)
    else return ("%dm"):format(m) end
end

local function CreateStatsTitleBar(parent, labelText)
    local title = CreateFont(parent, 13, COLORS.text)
    title:SetPoint("TOPLEFT", 16, -12)
    title:SetText(labelText)
    local line = parent:CreateTexture(nil, "ARTWORK")
    line:SetTexture(WHITE_TEXTURE)
    line:SetPoint("BOTTOMLEFT", title, "BOTTOMLEFT", 0, -4)
    line:SetPoint("BOTTOMRIGHT", title, "BOTTOMRIGHT", 0, -4)
    line:SetHeight(1)
    line:SetVertexColor(COLORS.accent[1], COLORS.accent[2], COLORS.accent[3], 0.4)
    return title
end

local function CreateStatsCard(parent, y, w, h)
    local card = CreateFrame("Frame", nil, parent)
    card:SetPoint("TOPLEFT", STATS_PAD, y)
    card:SetSize(w, h)
    MythicTools:ApplySurface(card, COLORS.sectionBG, COLORS.surface, COLORS.accentSoft)
    ClipChildren(card)
    return card
end

local function UpdateDungeonRowBar(row, d, maxCount)
    local barW = row.barW or 200
    local fill = maxCount > 0 and (barW * d.count / maxCount) or 0
    local tw = d.count > 0 and (fill * d.timed / d.count) or 0
    local ow = d.count > 0 and (fill * d.overtime / d.count) or 0
    local aw = d.count > 0 and (fill * d.abandoned / d.count) or 0
    row.BarTimed:SetWidth(max(0, tw))
    row.BarOT:ClearAllPoints()
    row.BarOT:SetPoint("TOPLEFT", row.BarTimed, "TOPRIGHT", 0, 0)
    row.BarOT:SetWidth(max(0, ow))
    row.BarAb:ClearAllPoints()
    row.BarAb:SetPoint("TOPLEFT", row.BarOT, "TOPRIGHT", 0, 0)
    row.BarAb:SetWidth(max(0, aw))
end

local function CreateStatsDungeonRow(parent, index, rowW)
    local row = CreateFrame("Frame", nil, parent)
    local rowY = -((index - 1) * STATS_DUNGEON_ROW_H + STATS_SECTION_TITLE_H)
    row:SetPoint("TOPLEFT", 0, rowY)
    row:SetSize(rowW, STATS_DUNGEON_ROW_H)
    row:Hide()
    if index % 2 == 0 then
        local bg = row:CreateTexture(nil, "BACKGROUND")
        bg:SetAllPoints()
        bg:SetTexture(WHITE_TEXTURE)
        bg:SetVertexColor(0.07, 0.07, 0.07, 1)
    end
    row.Name = CreateFont(row, 11, COLORS.text)
    row.Name:SetPoint("LEFT", 16, 0)
    row.Name:SetWidth(196)
    row.Count = CreateFont(row, 11, COLORS.accent, "RIGHT")
    row.Count:SetPoint("RIGHT", -16, 0)
    row.Count:SetWidth(40)
    row.Status = CreateFont(row, 9, COLORS.subdued, "RIGHT")
    row.Status:SetPoint("RIGHT", row.Count, "LEFT", -6, 0)
    row.Status:SetWidth(140)
    -- barW: rowW minus fixed left/right elements
    -- LEFT(16) + nameW(196) + gap(10) + [bar] + gap(10) + statusW(140) + gap(6) + countW(40) + RIGHT(16)
    local barW = max(10, rowW - 434)
    row.barW = barW
    local barHolder = CreateFrame("Frame", nil, row)
    barHolder:SetSize(barW, STATS_BAR_H)
    barHolder:SetPoint("LEFT", row.Name, "RIGHT", 10, 0)
    local barBg = barHolder:CreateTexture(nil, "BACKGROUND")
    barBg:SetAllPoints()
    barBg:SetTexture(WHITE_TEXTURE)
    barBg:SetVertexColor(0.07, 0.07, 0.07, 1)
    row.BarTimed = barHolder:CreateTexture(nil, "ARTWORK")
    row.BarTimed:SetTexture(WHITE_TEXTURE)
    row.BarTimed:SetVertexColor(COLORS.success[1], COLORS.success[2], COLORS.success[3], 0.85)
    row.BarTimed:SetPoint("TOPLEFT")
    row.BarTimed:SetHeight(STATS_BAR_H)
    row.BarTimed:SetWidth(0)
    row.BarOT = barHolder:CreateTexture(nil, "ARTWORK")
    row.BarOT:SetTexture(WHITE_TEXTURE)
    row.BarOT:SetVertexColor(COLORS.warning[1], COLORS.warning[2], COLORS.warning[3], 0.85)
    row.BarOT:SetPoint("TOPLEFT", row.BarTimed, "TOPRIGHT", 0, 0)
    row.BarOT:SetHeight(STATS_BAR_H)
    row.BarOT:SetWidth(0)
    row.BarAb = barHolder:CreateTexture(nil, "ARTWORK")
    row.BarAb:SetTexture(WHITE_TEXTURE)
    row.BarAb:SetVertexColor(COLORS.danger[1], COLORS.danger[2], COLORS.danger[3], 0.85)
    row.BarAb:SetPoint("TOPLEFT", row.BarOT, "TOPRIGHT", 0, 0)
    row.BarAb:SetHeight(STATS_BAR_H)
    row.BarAb:SetWidth(0)
    row.BarHolder = barHolder
    return row
end

function MythicTools:ComputeStatsData()
    local runs = (self.db and self.db.runs) or {}
    local total = #runs
    local timed, overtime, abandoned = 0, 0, 0
    local totalLevel, countedLevel, bestTimed = 0, 0, 0
    local totalTimeMS, totalDeaths = 0, 0
    local dungeons, classes, teammates = {}, {}, {}

    for _, run in ipairs(runs) do
        if type(run) == "table" then
            local result = run.result or "abandoned"
            local isAbandoned = result == "abandoned"
            local isTimed = result == "timed"
            if isTimed then timed = timed + 1
            elseif isAbandoned then abandoned = abandoned + 1
            else overtime = overtime + 1 end
            if not isAbandoned and (run.level or 0) > 0 then
                totalLevel = totalLevel + run.level
                countedLevel = countedLevel + 1
            end
            if isTimed and (run.level or 0) > bestTimed then bestTimed = run.level end
            if not isAbandoned then totalTimeMS = totalTimeMS + (run.timeMS or 0) end
            totalDeaths = totalDeaths + (run.totalDeaths or 0)
            local dName = type(run.dungeonName) == "string" and run.dungeonName ~= "" and run.dungeonName
            if dName then
                if not dungeons[dName] then
                    dungeons[dName] = {name=dName, count=0, timed=0, overtime=0, abandoned=0, totalTimeMS=0, countedTime=0}
                end
                local d = dungeons[dName]
                d.count = d.count + 1
                if isTimed then d.timed = d.timed + 1
                elseif isAbandoned then d.abandoned = d.abandoned + 1
                else d.overtime = d.overtime + 1 end
                if not isAbandoned and (run.timeMS or 0) > 0 then
                    d.totalTimeMS = d.totalTimeMS + run.timeMS
                    d.countedTime = d.countedTime + 1
                end
            end
            for playerName, stat in pairs(run.playerStats or {}) do
                if type(stat) == "table" and not self:IsOwnedCharacter(playerName) then
                    if stat.classFilename then
                        classes[stat.classFilename] = (classes[stat.classFilename] or 0) + 1
                    end
                    local sn = stat.shortName or self:GetShortName(playerName)
                    if sn and sn ~= "" then
                        if not teammates[sn] then
                            teammates[sn] = {name=sn, count=0, classFilename=stat.classFilename}
                        end
                        teammates[sn].count = teammates[sn].count + 1
                        if stat.classFilename then teammates[sn].classFilename = stat.classFilename end
                    end
                end
            end
        end
    end

    local sortedDungeons = {}
    for _, d in pairs(dungeons) do
        d.avgTimeMS = d.countedTime > 0 and (d.totalTimeMS / d.countedTime) or 0
        sortedDungeons[#sortedDungeons + 1] = d
    end
    table.sort(sortedDungeons, function(a, b) return a.count > b.count end)

    local sortedClasses = {}
    for cf, cnt in pairs(classes) do
        sortedClasses[#sortedClasses + 1] = {classFilename=cf, count=cnt}
    end
    table.sort(sortedClasses, function(a, b) return a.count > b.count end)

    local sortedTeammates = {}
    for _, t in pairs(teammates) do sortedTeammates[#sortedTeammates + 1] = t end
    table.sort(sortedTeammates, function(a, b) return a.count > b.count end)

    return {
        total = total, timed = timed, overtime = overtime, abandoned = abandoned,
        timedPct = total > 0 and (timed / total * 100) or 0,
        avgLevel = countedLevel > 0 and (totalLevel / countedLevel) or 0,
        bestTimed = bestTimed, totalTimeMS = totalTimeMS, totalDeaths = totalDeaths,
        dungeons = sortedDungeons, classes = sortedClasses, teammates = sortedTeammates,
    }
end

function MythicTools:BuildStatsPage(parent)
    local page = CreateFrame("Frame", nil, parent)
    page:SetAllPoints(parent)
    ClipChildren(page)

    -- Header
    local headerCard = CreateFrame("Frame", nil, page)
    headerCard:SetPoint("TOPLEFT", 12, -12)
    headerCard:SetPoint("TOPRIGHT", -12, -12)
    headerCard:SetHeight(38)
    self:ApplySurface(headerCard, COLORS.sectionBG, COLORS.surface, COLORS.accentSoft)
    local headerTitle = CreateFont(headerCard, 13, COLORS.accent)
    headerTitle:SetPoint("LEFT", 16, 0)
    headerTitle:SetText("Statistics")

    -- Scroll frame
    local scrollFrame = CreateFrame("ScrollFrame", nil, page)
    scrollFrame:SetPoint("TOPLEFT", 12, -62)
    scrollFrame:SetPoint("BOTTOMRIGHT", -20, 12)
    scrollFrame:EnableMouseWheel(true)
    self.ui.statsScrollFrame = scrollFrame

    -- Explicit content width: SetScrollChild breaks TOPRIGHT anchors (they resolve to 0).
    -- Derive from saved frame width: pageW = frameW - sidebarW(213), scrollFrame inset = 12+20 = 32
    local frameW = (self.db and self.db.ui and self.db.ui.width) or 1080
    local scrollW = max(600, frameW - 245)
    local sectionW = scrollW - 2 * STATS_PAD
    local leftColW = floor(sectionW * 0.37)
    local rightColW = sectionW - leftColW - STATS_PAD

    local content = CreateFrame("Frame", nil, scrollFrame)
    content:SetSize(scrollW, STATS_CONTENT_H)
    scrollFrame:SetScrollChild(content)
    self.ui.statsContent = content

    -- Thin scrollbar
    local scrollBar = CreateFrame("Slider", nil, page)
    scrollBar:SetOrientation("VERTICAL")
    scrollBar:SetWidth(6)
    scrollBar:SetPoint("TOPRIGHT", page, "TOPRIGHT", -12, -62)
    scrollBar:SetPoint("BOTTOMRIGHT", page, "BOTTOMRIGHT", -12, 12)
    scrollBar:SetMinMaxValues(0, 1)
    scrollBar:SetValue(0)
    scrollBar:Hide()
    local sbThumb = scrollBar:CreateTexture(nil, "ARTWORK")
    sbThumb:SetTexture(WHITE_TEXTURE)
    sbThumb:SetWidth(4)
    sbThumb:SetVertexColor(COLORS.accent[1], COLORS.accent[2], COLORS.accent[3], 0.65)
    scrollBar:SetThumbTexture(sbThumb)
    scrollBar:SetScript("OnValueChanged", function(self, value)
        scrollFrame:SetVerticalScroll(value)
    end)
    scrollFrame:SetScript("OnScrollRangeChanged", function(self, _, yRange)
        local m = max(0, yRange)
        scrollBar:SetMinMaxValues(0, m)
        scrollBar:SetShown(m > 0)
    end)
    scrollFrame:SetScript("OnMouseWheel", function(self, delta)
        local cur = scrollBar:GetValue()
        local _, maxVal = scrollBar:GetMinMaxValues()
        scrollBar:SetValue(max(0, min(cur - delta * 60, maxVal)))
    end)
    self.ui.statsScrollBar = scrollBar

    local y = -STATS_PAD

    -- ── SECTION 1: OVERVIEW ──────────────────────────────────────────────
    local OVERVIEW_LABELS = {"Total Runs", "Timed %", "Avg Key", "Best Key", "In-Key Time", "Deaths"}
    local NUM_OV = #OVERVIEW_LABELS
    local ovGap = 8
    local ovCardW = floor((sectionW - (NUM_OV - 1) * ovGap) / NUM_OV)
    local overviewRow = CreateFrame("Frame", nil, content)
    overviewRow:SetPoint("TOPLEFT", STATS_PAD, y)
    overviewRow:SetSize(sectionW, STATS_OVERVIEW_H)
    self.ui.statsOverviewCards = {}
    for i = 1, NUM_OV do
        local card = CreateFrame("Frame", nil, overviewRow)
        self:ApplySurface(card, COLORS.sectionBG, COLORS.surface, COLORS.accentSoft)
        card:SetPoint("TOPLEFT", (i - 1) * (ovCardW + ovGap), 0)
        card:SetSize(ovCardW, STATS_OVERVIEW_H)
        card.Value = CreateFont(card, 22, COLORS.text, "CENTER")
        card.Value:SetPoint("CENTER", 0, 12)
        card.Value:SetText("--")
        card.Label = CreateFont(card, 10, COLORS.subdued, "CENTER")
        card.Label:SetPoint("CENTER", 0, -12)
        card.Label:SetText(OVERVIEW_LABELS[i])
        self.ui.statsOverviewCards[i] = card
    end

    y = y - STATS_OVERVIEW_H - STATS_PAD

    -- ── SECTION 2: MOST PLAYED DUNGEONS ──────────────────────────────────
    local dungeonsCard = CreateStatsCard(content, y, sectionW, STATS_DUNGEON_SECTION_H)
    CreateStatsTitleBar(dungeonsCard, "Most Played Dungeons")
    self.ui.statsDungeonRows = {}
    for i = 1, MAX_STATS_DUNGEON_ROWS do
        self.ui.statsDungeonRows[i] = CreateStatsDungeonRow(dungeonsCard, i, sectionW)
    end

    y = y - STATS_DUNGEON_SECTION_H - STATS_PAD

    -- ── SECTION 3: TWO COLUMNS ────────────────────────────────────────────
    -- Left: Team Classes
    local classesCard = CreateFrame("Frame", nil, content)
    classesCard:SetPoint("TOPLEFT", STATS_PAD, y)
    classesCard:SetSize(leftColW, STATS_TWOCOL_H)
    self:ApplySurface(classesCard, COLORS.sectionBG, COLORS.surface, COLORS.accentSoft)
    ClipChildren(classesCard)
    CreateStatsTitleBar(classesCard, "Team Classes")
    self.ui.statsClassRows = {}
    for i = 1, MAX_STATS_CLASS_ROWS do
        local row = CreateFrame("Frame", nil, classesCard)
        local rowY = -((i - 1) * STATS_CLASS_ROW_H + STATS_SECTION_TITLE_H)
        row:SetPoint("TOPLEFT", 0, rowY)
        row:SetSize(leftColW, STATS_CLASS_ROW_H)
        row:Hide()
        if i % 2 == 0 then
            local bg = row:CreateTexture(nil, "BACKGROUND")
            bg:SetAllPoints()
            bg:SetTexture(WHITE_TEXTURE)
            bg:SetVertexColor(0.07, 0.07, 0.07, 1)
        end
        row.Dot = row:CreateTexture(nil, "ARTWORK")
        row.Dot:SetTexture(WHITE_TEXTURE)
        row.Dot:SetSize(9, 9)
        row.Dot:SetPoint("LEFT", 16, 0)
        row.Name = CreateFont(row, 11, COLORS.text)
        row.Name:SetPoint("LEFT", row.Dot, "RIGHT", 8, 0)
        row.Name:SetWidth(130)
        row.Count = CreateFont(row, 11, COLORS.accent, "RIGHT")
        row.Count:SetPoint("RIGHT", -16, 0)
        self.ui.statsClassRows[i] = row
    end

    -- Right: Avg Completion Time (TOPLEFT anchor from left col edge, explicit size)
    local avgTimeCard = CreateFrame("Frame", nil, content)
    avgTimeCard:SetPoint("TOPLEFT", STATS_PAD + leftColW + STATS_PAD, y)
    avgTimeCard:SetSize(rightColW, STATS_TWOCOL_H)
    self:ApplySurface(avgTimeCard, COLORS.sectionBG, COLORS.surface, COLORS.accentSoft)
    ClipChildren(avgTimeCard)
    CreateStatsTitleBar(avgTimeCard, "Avg Completion Time")
    self.ui.statsAvgTimeRows = {}
    for i = 1, MAX_STATS_DUNGEON_ROWS do
        local row = CreateFrame("Frame", nil, avgTimeCard)
        local rowY = -((i - 1) * STATS_DUNGEON_ROW_H + STATS_SECTION_TITLE_H)
        row:SetPoint("TOPLEFT", 0, rowY)
        row:SetSize(rightColW, STATS_DUNGEON_ROW_H)
        row:Hide()
        if i % 2 == 0 then
            local bg = row:CreateTexture(nil, "BACKGROUND")
            bg:SetAllPoints()
            bg:SetTexture(WHITE_TEXTURE)
            bg:SetVertexColor(0.07, 0.07, 0.07, 1)
        end
        row.Name = CreateFont(row, 11, COLORS.text)
        row.Name:SetPoint("LEFT", 16, 0)
        row.Name:SetWidth(200)
        row.Time = CreateFont(row, 12, COLORS.accent, "RIGHT")
        row.Time:SetPoint("RIGHT", -16, 0)
        self.ui.statsAvgTimeRows[i] = row
    end

    y = y - STATS_TWOCOL_H - STATS_PAD

    -- ── SECTION 4: MOST PLAYED WITH ───────────────────────────────────────
    local teammatesCard = CreateStatsCard(content, y, sectionW, STATS_TEAMMATE_SECTION_H)
    CreateStatsTitleBar(teammatesCard, "Most Played With")
    self.ui.statsTeammateRows = {}
    for i = 1, MAX_STATS_TEAMMATE_ROWS do
        local row = CreateFrame("Frame", nil, teammatesCard)
        local rowY = -((i - 1) * STATS_TEAMMATE_ROW_H + STATS_SECTION_TITLE_H)
        row:SetPoint("TOPLEFT", 0, rowY)
        row:SetSize(sectionW, STATS_TEAMMATE_ROW_H)
        row:Hide()
        if i % 2 == 0 then
            local bg = row:CreateTexture(nil, "BACKGROUND")
            bg:SetAllPoints()
            bg:SetTexture(WHITE_TEXTURE)
            bg:SetVertexColor(0.07, 0.07, 0.07, 1)
        end
        local rankBg = CreateFrame("Frame", nil, row)
        rankBg:SetSize(22, 22)
        rankBg:SetPoint("LEFT", 14, 0)
        MythicTools:ApplySurface(rankBg, COLORS.frameBG, COLORS.surface, nil)
        row.Rank = CreateFont(rankBg, 9, COLORS.subdued, "CENTER")
        row.Rank:SetAllPoints()
        row.Rank:SetText(tostring(i))
        row.Dot = row:CreateTexture(nil, "ARTWORK")
        row.Dot:SetTexture(WHITE_TEXTURE)
        row.Dot:SetSize(10, 10)
        row.Dot:SetPoint("LEFT", rankBg, "RIGHT", 10, 0)
        row.Name = CreateFont(row, 12, COLORS.text)
        row.Name:SetPoint("LEFT", row.Dot, "RIGHT", 8, 0)
        row.Name:SetWidth(200)
        row.Class = CreateFont(row, 10, COLORS.subdued)
        row.Class:SetPoint("LEFT", row.Name, "RIGHT", 10, 0)
        row.Class:SetWidth(110)
        row.Runs = CreateFont(row, 12, COLORS.accent, "RIGHT")
        row.Runs:SetPoint("RIGHT", -16, 0)
        self.ui.statsTeammateRows[i] = row
    end

    return page
end

function MythicTools:RefreshStatsView()
    if not (self.ui and self.ui.statsContent) then return end
    if not (self.ui.pages and self.ui.pages.Stats and self.ui.pages.Stats:IsShown()) then return end

    local data = self:ComputeStatsData()

    -- Overview
    local cards = self.ui.statsOverviewCards
    if cards then
        local function sv(i, val) if cards[i] then cards[i].Value:SetText(val) end end
        sv(1, tostring(data.total))
        sv(2, data.total > 0 and string.format("%.1f%%", data.timedPct) or "--")
        sv(3, data.avgLevel > 0 and string.format("%.1f", data.avgLevel) or "--")
        sv(4, data.bestTimed > 0 and ("+%d"):format(data.bestTimed) or "--")
        sv(5, FormatTotalTime(data.totalTimeMS))
        sv(6, tostring(data.totalDeaths))
    end

    -- Dungeon rows
    local maxDungCount = (data.dungeons[1] and data.dungeons[1].count) or 1
    for i, row in ipairs(self.ui.statsDungeonRows or {}) do
        local d = data.dungeons[i]
        if d then
            row.Name:SetText(d.name)
            row.Count:SetText(tostring(d.count))
            row.Status:SetText(("T:%d  OT:%d  Ab:%d"):format(d.timed, d.overtime, d.abandoned))
            UpdateDungeonRowBar(row, d, maxDungCount)
            row:Show()
        else
            row:Hide()
        end
    end

    -- Class rows
    for i, row in ipairs(self.ui.statsClassRows or {}) do
        local c = data.classes[i]
        if c then
            local localName = (LOCALIZED_CLASS_NAMES_MALE and LOCALIZED_CLASS_NAMES_MALE[c.classFilename]) or c.classFilename or "?"
            local cc = RAID_CLASS_COLORS and RAID_CLASS_COLORS[c.classFilename]
            if cc then
                row.Dot:SetVertexColor(cc.r, cc.g, cc.b, 1)
                SetTextColor(row.Name, {cc.r, cc.g, cc.b, 1})
            else
                row.Dot:SetVertexColor(0.7, 0.7, 0.7, 1)
                SetTextColor(row.Name, COLORS.text)
            end
            row.Name:SetText(localName)
            row.Count:SetText(tostring(c.count))
            row:Show()
        else
            row:Hide()
        end
    end

    -- Avg time rows (same dungeon order)
    for i, row in ipairs(self.ui.statsAvgTimeRows or {}) do
        local d = data.dungeons[i]
        if d then
            row.Name:SetText(d.name)
            row.Time:SetText(d.avgTimeMS > 0 and self:FormatDurationMS(d.avgTimeMS) or "--")
            row:Show()
        else
            row:Hide()
        end
    end

    -- Teammate rows
    for i, row in ipairs(self.ui.statsTeammateRows or {}) do
        local t = data.teammates[i]
        if t then
            local cc = t.classFilename and RAID_CLASS_COLORS and RAID_CLASS_COLORS[t.classFilename]
            if cc then
                row.Dot:SetVertexColor(cc.r, cc.g, cc.b, 1)
            else
                row.Dot:SetVertexColor(0.6, 0.6, 0.6, 1)
            end
            row.Name:SetText(t.name)
            local localClass = (t.classFilename and LOCALIZED_CLASS_NAMES_MALE and LOCALIZED_CLASS_NAMES_MALE[t.classFilename]) or ""
            row.Class:SetText(localClass)
            row.Runs:SetText(("%d runs"):format(t.count))
            row:Show()
        else
            row:Hide()
        end
    end

    if self.ui.statsScrollFrame then self.ui.statsScrollFrame:SetVerticalScroll(0) end
    if self.ui.statsScrollBar then self.ui.statsScrollBar:SetValue(0) end
end

function MythicTools:BuildRunsPage(parent)
    local page = CreateFrame("Frame", nil, parent)
    page:SetAllPoints(parent)
    ClipChildren(page)

    local filterBar = CreateFrame("Frame", nil, page)
    filterBar:SetPoint("TOPLEFT", 12, -12)
    filterBar:SetPoint("TOPRIGHT", -12, -12)
    filterBar:SetHeight(154)
    self:ApplySurface(filterBar, COLORS.sectionBG, COLORS.surface, COLORS.accentSoft)
    self.ui.runsFilterBar = filterBar

    local filterTitle = CreateFont(filterBar, 13, COLORS.text)
    filterTitle:SetPoint("TOPLEFT", 16, -14)
    filterTitle:SetText("Filters")

    self.ui.runCountLabel = CreateFont(filterBar, 12, COLORS.muted, "RIGHT")
    self.ui.runCountLabel:SetPoint("TOPRIGHT", -16, -14)

    local topRow = CreateAceFlowRow(filterBar, -34, 54)
    local bottomRow = CreateAceFlowRow(filterBar, -92, 54)

    self.ui.runsSearch = CreateAceEditWidget("Search", 210, self.db.ui.filters.search, function(value)
        self.db.ui.filters.search = value or ""
        self:SetRunViewMode("list")
        self:RefreshRunView()
    end)
    StyleAceEditWidget(self.ui.runsSearch)
    topRow:AddChild(self.ui.runsSearch)
    topRow:AddChild(CreateAceSpacer(10))

    self.ui.runsPlayerFilter = CreateAceEditWidget("Player", 190, self.db.ui.filters.player, function(value)
        self.db.ui.filters.player = value or ""
        self:SetRunViewMode("list")
        self:RefreshRunView()
    end)
    StyleAceEditWidget(self.ui.runsPlayerFilter)
    topRow:AddChild(self.ui.runsPlayerFilter)
    topRow:AddChild(CreateAceSpacer(10))

    self.ui.runsDateFilter = CreateAceEditWidget("Date", 120, self.db.ui.filters.date, function(value)
        self.db.ui.filters.date = value or ""
        self:SetRunViewMode("list")
        self:RefreshRunView()
    end)
    StyleAceEditWidget(self.ui.runsDateFilter)
    topRow:AddChild(self.ui.runsDateFilter)
    topRow:AddChild(CreateAceSpacer(10))

    self.ui.runsStatusFilter = CreateAceDropdownWidget("Status", 140, STATUS_OPTIONS, self.db.ui.filters.status, function(value)
        self.db.ui.filters.status = value or "all"
        self:SetRunViewMode("list")
        self:RefreshRunView()
    end)
    StyleAceDropdownWidget(self.ui.runsStatusFilter)
    topRow:AddChild(self.ui.runsStatusFilter)

    self.ui.runsSeasonFilter = CreateAceDropdownWidget("Season", 170, SEASON_OPTIONS, self.db.ui.filters.season, function(value)
        self.db.ui.filters.season = value or SEASON_OPTIONS[1].value
        self.db.ui.filters.dungeon = "all"
        SetAceDropdownValue(self.ui.runsSeasonFilter, self.db.ui.filters.season)
        UpdateAceDropdownOptions(self.ui.runsDungeonFilter, GetDungeonOptionsForSeason(self.db.ui.filters.season), self.db.ui.filters.dungeon)
        self:SetRunViewMode("list")
        self:RefreshRunView()
    end)
    StyleAceDropdownWidget(self.ui.runsSeasonFilter)
    bottomRow:AddChild(self.ui.runsSeasonFilter)
    bottomRow:AddChild(CreateAceSpacer(10))

    self.ui.runsDungeonFilter = CreateAceDropdownWidget("Dungeon", 260, GetDungeonOptionsForSeason(self.db.ui.filters.season), self.db.ui.filters.dungeon, function(value)
        self.db.ui.filters.dungeon = value or "all"
        self:SetRunViewMode("list")
        self:RefreshRunView()
    end)
    StyleAceDropdownWidget(self.ui.runsDungeonFilter)
    bottomRow:AddChild(self.ui.runsDungeonFilter)
    bottomRow:AddChild(CreateAceSpacer(10))

    self.ui.runsOwnedFilter = CreateAceDropdownWidget("Characters", 180, GetOwnedCharacterFilterOptions(), self.db.ui.filters.ownedCharacters or "all", function(value)
        self.db.ui.filters.ownedCharacters = value or "all"
        self:SetRunViewMode("list")
        self:RefreshRunView()
    end)
    StyleAceDropdownWidget(self.ui.runsOwnedFilter)
    bottomRow:AddChild(self.ui.runsOwnedFilter)

    self.ui.runsClearButton = AceGUI and AceGUI:Create("Button") or nil
    if self.ui.runsClearButton then
        self.ui.runsClearButton:SetText("Clear")
        self.ui.runsClearButton:SetWidth(86)
        self.ui.runsClearButton:SetCallback("OnClick", function()
            self.db.ui.filters.search = ""
            self.db.ui.filters.player = ""
            self.db.ui.filters.dungeon = "all"
            self.db.ui.filters.season = "season1"
            self.db.ui.filters.date = ""
            self.db.ui.filters.status = "all"
            self.db.ui.filters.ownedCharacters = "all"
            SetAceEditText(self.ui.runsSearch, "")
            SetAceEditText(self.ui.runsPlayerFilter, "")
            SetAceEditText(self.ui.runsDateFilter, "")
            SetAceDropdownValue(self.ui.runsSeasonFilter, "season1")
            UpdateAceDropdownOptions(self.ui.runsDungeonFilter, GetDungeonOptionsForSeason("season1"), "all")
            SetAceDropdownValue(self.ui.runsStatusFilter, "all")
            SetAceDropdownValue(self.ui.runsOwnedFilter, "all")
            self:SetRunViewMode("list")
            self:RefreshRunView()
        end)
        bottomRow:AddChild(self.ui.runsClearButton)
    end

    self.ui.runsExportButton = AceGUI and AceGUI:Create("Button") or nil
    if self.ui.runsExportButton then
        self.ui.runsExportButton:SetText("Export CSV")
        self.ui.runsExportButton:SetWidth(110)
        self.ui.runsExportButton:SetCallback("OnClick", function()
            self:ShowCSVExportPopup()
        end)
        bottomRow:AddChild(CreateAceSpacer(10))
        bottomRow:AddChild(self.ui.runsExportButton)
    end

    if not self.ui.runsClearButton then
        local clearButton = CreateActionButton(filterBar, 86, 28, "Clear")
        clearButton:SetPoint("TOPLEFT", 460, -110)
        clearButton:SetScript("OnClick", function()
            self.db.ui.filters.search = ""
            self.db.ui.filters.player = ""
            self.db.ui.filters.dungeon = "all"
            self.db.ui.filters.season = "season1"
            self.db.ui.filters.date = ""
            self.db.ui.filters.status = "all"
            self.db.ui.filters.ownedCharacters = "all"
            SetAceEditText(self.ui.runsSearch, "")
            SetAceEditText(self.ui.runsPlayerFilter, "")
            SetAceEditText(self.ui.runsDateFilter, "")
            SetAceDropdownValue(self.ui.runsSeasonFilter, "season1")
            UpdateAceDropdownOptions(self.ui.runsDungeonFilter, GetDungeonOptionsForSeason("season1"), "all")
            SetAceDropdownValue(self.ui.runsStatusFilter, "all")
            SetAceDropdownValue(self.ui.runsOwnedFilter, "all")
            self:SetRunViewMode("list")
            self:RefreshRunView()
        end)

        local exportButton = CreateActionButton(filterBar, 110, 28, "Export CSV")
        exportButton:SetPoint("LEFT", clearButton, "RIGHT", 10, 0)
        exportButton:SetScript("OnClick", function()
            self:ShowCSVExportPopup()
        end)
    end

    local listView = CreateFrame("Frame", nil, page)
    listView:SetPoint("TOPLEFT", filterBar, "BOTTOMLEFT", 0, -12)
    listView:SetPoint("BOTTOMRIGHT", page, "BOTTOMRIGHT", -12, 12)
    self:ApplySurface(listView, COLORS.sectionBG, COLORS.surface, COLORS.accentSoft)
    ClipChildren(listView)
    self.ui.runsListView = listView

    local listTitle = CreateFont(listView, 14, COLORS.text)
    listTitle:SetPoint("TOPLEFT", 16, -14)
    listTitle:SetText("Key history")

    self.ui.runsEmptyState = CreateFont(listView, 13, COLORS.muted, "CENTER")
    self.ui.runsEmptyState:SetPoint("CENTER")
    self.ui.runsEmptyState:SetText("No runs match the current filters.")
    self.ui.runsEmptyState:Hide()

    local listBody = CreateFrame("Frame", nil, listView)
    listBody:SetPoint("TOPLEFT", 10, -44)
    listBody:SetPoint("TOPRIGHT", -22, -44)
    listBody:SetHeight(RUN_ROWS * RUN_ROW_HEIGHT + 2)
    ClipChildren(listBody)
    self.ui.runsListBody = listBody

    self.ui.runsScroll = CreateFrame("ScrollFrame", nil, listView, "FauxScrollFrameTemplate")
    self.ui.runsScroll:SetPoint("TOPLEFT", 0, -44)
    self.ui.runsScroll:SetPoint("BOTTOMRIGHT", -10, 14)
    self.ui.runsScroll:SetScript("OnVerticalScroll", function(scrollFrame, offset)
        FauxScrollFrame_OnVerticalScroll(scrollFrame, offset, RUN_ROW_HEIGHT, function()
            self:RefreshRunView()
        end)
    end)
    ApplyThinScrollBar(self.ui.runsScroll)

    self.ui.runRows = {}
    for index = 1, RUN_ROWS do
        local row = CreateListButton(listBody, RUN_ROW_HEIGHT - 4)
        row:SetPoint("TOPLEFT", 0, -((index - 1) * RUN_ROW_HEIGHT))
        row:SetPoint("TOPRIGHT", 0, -((index - 1) * RUN_ROW_HEIGHT))
        row:SetScript("OnClick", function(button, mouseButton)
            if not button.runId then
                return
            end

            if mouseButton == "RightButton" then
                self:ConfirmDeleteRun(button.runId)
            else
                self.db.ui.selectedRunId = button.runId
                self:RefreshRunView()
                self:ShowCompletionPopup(button.runId)
            end
        end)
        self.ui.runRows[index] = row
    end

    local detailPanel = CreateFrame("Frame", nil, page)
    detailPanel:SetPoint("TOPLEFT", page, "TOPLEFT", 12, -12)
    detailPanel:SetPoint("BOTTOMRIGHT", page, "BOTTOMRIGHT", -12, 12)
    self:ApplySurface(detailPanel, COLORS.sectionBG, COLORS.surface, COLORS.accentSoft)
    ClipChildren(detailPanel)
    self.ui.runsDetailView = detailPanel

    local detailHeader = CreateFrame("Frame", nil, detailPanel)
    detailHeader:SetPoint("TOPLEFT", 16, -16)
    detailHeader:SetPoint("TOPRIGHT", -16, -16)
    detailHeader:SetHeight(38)

    self.ui.runDetailBackButton = CreateActionButton(detailHeader, 136, 28, "Back to history")
    self.ui.runDetailBackButton:SetPoint("LEFT", 0, 0)
    self.ui.runDetailBackButton:SetScript("OnClick", function()
        self:SetRunViewMode("list")
        self:RefreshRunView()
    end)

    local detailHeaderText = CreateFont(detailHeader, 12, COLORS.muted)
    detailHeaderText:SetPoint("LEFT", self.ui.runDetailBackButton, "RIGHT", 14, 0)
    detailHeaderText:SetText("Run details")

    local hero = CreateFrame("Frame", nil, detailPanel)
    hero:SetPoint("TOPLEFT", 16, -62)
    hero:SetPoint("TOPRIGHT", -16, -62)
    hero:SetHeight(150)
    self:ApplySurface(hero, COLORS.headerBG, COLORS.surface, COLORS.accent)
    self.ui.runHero = hero

    hero.Background = hero:CreateTexture(nil, "BACKGROUND")
    hero.Background:SetAllPoints()
    hero.Background:SetAlpha(0.18)
    self.ui.runHeroBackground = hero.Background

    hero.Shade = hero:CreateTexture(nil, "BORDER")
    hero.Shade:SetAllPoints()
    hero.Shade:SetTexture(WHITE_TEXTURE)
    hero.Shade:SetVertexColor(0.01, 0.02, 0.03, 0.44)

    local iconFrame = CreateFrame("Frame", nil, hero)
    iconFrame:SetSize(70, 70)
    iconFrame:SetPoint("TOPLEFT", 18, -18)
    self:ApplySurface(iconFrame, COLORS.frameBG, COLORS.accentSoft, COLORS.accent)

    iconFrame.Icon = iconFrame:CreateTexture(nil, "ARTWORK")
    iconFrame.Icon:SetPoint("TOPLEFT", 1, -1)
    iconFrame.Icon:SetPoint("BOTTOMRIGHT", -1, 1)
    self.ui.runHeroIcon = iconFrame.Icon

    self.ui.runDetailTitle = CreateFont(hero, 22, COLORS.text)
    self.ui.runDetailTitle:SetPoint("TOPLEFT", iconFrame, "TOPRIGHT", 14, -2)
    self.ui.runDetailTitle:SetPoint("RIGHT", hero, "RIGHT", -122, 0)

    self.ui.runDetailMeta = CreateFont(hero, 12, COLORS.muted)
    self.ui.runDetailMeta:SetPoint("TOPLEFT", self.ui.runDetailTitle, "BOTTOMLEFT", 0, -8)

    self.ui.runDetailSubmeta = CreateFont(hero, 11, COLORS.muted)
    self.ui.runDetailSubmeta:SetPoint("TOPLEFT", self.ui.runDetailMeta, "BOTTOMLEFT", 0, -6)
    self.ui.runDetailSubmeta:SetPoint("RIGHT", hero, "RIGHT", -16, 0)

    self.ui.runDetailStatus = CreateFont(hero, 14, COLORS.accent, "RIGHT")
    self.ui.runDetailStatus:SetPoint("TOPRIGHT", -18, -20)

    self.ui.runDetailLoot = CreateFont(hero, 11, COLORS.text)
    self.ui.runDetailLoot:SetPoint("TOPLEFT", iconFrame, "BOTTOMLEFT", 0, -10)
    self.ui.runDetailLoot:SetPoint("RIGHT", hero, "RIGHT", -18, 0)

    self.ui.runDetailNote = CreateFont(hero, 10, COLORS.subdued)
    self.ui.runDetailNote:SetPoint("TOPLEFT", self.ui.runDetailLoot, "BOTTOMLEFT", 0, -6)
    self.ui.runDetailNote:SetPoint("RIGHT", hero, "RIGHT", -18, 0)
    self.ui.runDetailNote:SetJustifyH("LEFT")

    local statsPanel = CreateFrame("Frame", nil, detailPanel)
    statsPanel:SetPoint("TOPLEFT", hero, "BOTTOMLEFT", 0, -10)
    statsPanel:SetPoint("BOTTOMRIGHT", detailPanel, "BOTTOMRIGHT", -16, 16)
    self:ApplySurface(statsPanel, COLORS.panelBG, COLORS.surface, nil)
    ClipChildren(statsPanel)

    local columnName = CreateFont(statsPanel, 11, COLORS.subdued)
    columnName:SetPoint("TOPLEFT", 16, -12)
    columnName:SetText("Player")

    local columnScore = CreateFont(statsPanel, 11, COLORS.subdued)
    columnScore:SetPoint("TOPLEFT", COMPACT_STATS_SCORE_X, -12)
    columnScore:SetText("M+ score")

    local columnItems = CreateFont(statsPanel, 11, COLORS.subdued)
    columnItems:SetPoint("TOPLEFT", COMPACT_STATS_ITEMS_X, -12)
    columnItems:SetText("Loot")

    local columnDamage = CreateFont(statsPanel, 11, COLORS.subdued)
    columnDamage:SetPoint("TOPLEFT", COMPACT_STATS_DAMAGE_X, -12)
    columnDamage:SetText("Damage")

    local columnDPS = CreateFont(statsPanel, 11, COLORS.subdued)
    columnDPS:SetPoint("TOPLEFT", COMPACT_STATS_DPS_X, -12)
    columnDPS:SetText("DPS")

    local columnHealing = CreateFont(statsPanel, 11, COLORS.subdued)
    columnHealing:SetPoint("TOPLEFT", COMPACT_STATS_HEALING_X, -12)
    columnHealing:SetText("Healing")

    local columnHPS = CreateFont(statsPanel, 11, COLORS.subdued)
    columnHPS:SetPoint("TOPLEFT", COMPACT_STATS_HPS_X, -12)
    columnHPS:SetText("HPS")

    local columnInterrupts = CreateFont(statsPanel, 11, COLORS.subdued)
    columnInterrupts:SetPoint("TOPLEFT", COMPACT_STATS_INTERRUPTS_X, -12)
    columnInterrupts:SetText("Interrupts")

    local columnDeaths = CreateFont(statsPanel, 11, COLORS.subdued)
    columnDeaths:SetPoint("TOPLEFT", COMPACT_STATS_DEATHS_X, -12)
    columnDeaths:SetText("Deaths")

    self.ui.runStatRows = {}
    for index = 1, 5 do
        local row = CreateStatRow(statsPanel)
        row:SetPoint("TOPLEFT", 12, -34 - ((index - 1) * COMPLETION_ROW_HEIGHT))
        row:SetPoint("TOPRIGHT", -12, -34 - ((index - 1) * COMPLETION_ROW_HEIGHT))
        self.ui.runStatRows[index] = row
    end

    return page
end
function MythicTools:BuildPlayersPage(parent)
    local page = CreateFrame("Frame", nil, parent)
    page:SetAllPoints(parent)
    ClipChildren(page)

    local indexView = CreateFrame("Frame", nil, page)
    indexView:SetAllPoints(page)
    ClipChildren(indexView)
    self.ui.playersIndexView = indexView

    local filterBar = CreateFrame("Frame", nil, indexView)
    filterBar:SetPoint("TOPLEFT", 12, -12)
    filterBar:SetPoint("TOPRIGHT", -12, -12)
    filterBar:SetHeight(100)
    self:ApplySurface(filterBar, COLORS.sectionBG, COLORS.surface, COLORS.accentSoft)

    local topTitle = CreateFont(filterBar, 13, COLORS.text)
    topTitle:SetPoint("TOPLEFT", 16, -14)
    topTitle:SetText("Known players")

    self.ui.playerCountLabel = CreateFont(filterBar, 12, COLORS.muted, "RIGHT")
    self.ui.playerCountLabel:SetPoint("TOPRIGHT", -16, -14)

    local filtersRow = CreateAceFlowRow(filterBar, -34, 54)

    self.ui.playerSearch = CreateAceEditWidget("Search player", 280, (self.db.ui.playerFilters and self.db.ui.playerFilters.search) or "", function(value)
        self.db.ui.playerFilters.search = value or ""
        self.db.ui.playerSearch = value or ""
        self:RefreshPlayersView()
    end)
    StyleAceEditWidget(self.ui.playerSearch)
    filtersRow:AddChild(self.ui.playerSearch)
    filtersRow:AddChild(CreateAceSpacer(10))

    self.ui.playersSeasonFilter = CreateAceDropdownWidget("Season", 170, SEASON_OPTIONS, (self.db.ui.playerFilters and self.db.ui.playerFilters.season) or "season1", function(value)
        self.db.ui.playerFilters.season = value or "season1"
        self.db.ui.playerFilters.dungeon = "all"
        SetAceDropdownValue(self.ui.playersSeasonFilter, self.db.ui.playerFilters.season)
        UpdateAceDropdownOptions(self.ui.playersDungeonFilter, GetDungeonOptionsForSeason(self.db.ui.playerFilters.season), self.db.ui.playerFilters.dungeon)
        self:RefreshPlayersView()
    end)
    StyleAceDropdownWidget(self.ui.playersSeasonFilter)
    filtersRow:AddChild(self.ui.playersSeasonFilter)
    filtersRow:AddChild(CreateAceSpacer(10))

    self.ui.playersDungeonFilter = CreateAceDropdownWidget("Dungeon", 260, GetDungeonOptionsForSeason((self.db.ui.playerFilters and self.db.ui.playerFilters.season) or "season1"), (self.db.ui.playerFilters and self.db.ui.playerFilters.dungeon) or "all", function(value)
        self.db.ui.playerFilters.dungeon = value or "all"
        self:RefreshPlayersView()
    end)
    StyleAceDropdownWidget(self.ui.playersDungeonFilter)
    filtersRow:AddChild(self.ui.playersDungeonFilter)

    self.ui.playersClearFiltersButton = CreateActionButton(filterBar, 104, 28, "Clear filters")
    self.ui.playersClearFiltersButton:SetPoint("TOPRIGHT", -16, -52)
    self.ui.playersClearFiltersButton:SetScript("OnClick", function()
        self.db.ui.playerFilters.search = ""
        self.db.ui.playerFilters.season = "season1"
        self.db.ui.playerFilters.dungeon = "all"
        self.db.ui.playerSearch = ""
        SetAceEditText(self.ui.playerSearch, "")
        SetAceDropdownValue(self.ui.playersSeasonFilter, "season1")
        UpdateAceDropdownOptions(self.ui.playersDungeonFilter, GetDungeonOptionsForSeason("season1"), "all")
        self:RefreshPlayersView()
    end)

    local tableCard = CreateFrame("Frame", nil, indexView)
    tableCard:SetPoint("TOPLEFT", filterBar, "BOTTOMLEFT", 0, -12)
    tableCard:SetPoint("BOTTOMRIGHT", indexView, "BOTTOMRIGHT", -12, 12)
    self:ApplySurface(tableCard, COLORS.sectionBG, COLORS.surface, COLORS.accentSoft)
    ClipChildren(tableCard)

    local tableTitle = CreateFont(tableCard, 14, COLORS.text)
    tableTitle:SetPoint("TOPLEFT", 16, -14)
    tableTitle:SetText("Player analytics")

    self.ui.playersEmptyState = CreateFont(tableCard, 13, COLORS.muted, "CENTER")
    self.ui.playersEmptyState:SetPoint("CENTER")
    self.ui.playersEmptyState:SetText("No players match the current filters.")
    self.ui.playersEmptyState:Hide()

    self.ui.playersTableCard = tableCard
    self.ui.playersTable = self:CreatePlayersAnalyticsTable(tableCard)
    if self.ui.playersTable then
        ApplyThinScrollBarToST(self.ui.playersTable)
    end

    local detailView = CreateFrame("Frame", nil, page)
    detailView:SetAllPoints(page)
    ClipChildren(detailView)
    self.ui.playersDetailView = detailView

    self.ui.playerDetailBackButton = CreateActionButton(detailView, 138, 28, "Back to players")
    self.ui.playerDetailBackButton:SetPoint("TOPLEFT", 12, -12)
    self.ui.playerDetailBackButton:SetScript("OnClick", function()
        MythicTools:ShowPlayersIndex()
    end)

    self.ui.playerDetailRaiderIOButton = CreateActionButton(detailView, 132, 28, "Copy Raider.IO")
    self.ui.playerDetailRaiderIOButton:SetPoint("LEFT", self.ui.playerDetailBackButton, "RIGHT", 12, 0)
    self.ui.playerDetailRaiderIOButton:SetScript("OnClick", function(button)
        MythicTools:ShowRaiderIOCopyPopup(button.playerName or (MythicTools.db and MythicTools.db.ui and MythicTools.db.ui.selectedPlayer))
    end)

    self.ui.playerDetailHeader = CreateFont(detailView, 12, COLORS.muted)
    self.ui.playerDetailHeader:SetPoint("LEFT", self.ui.playerDetailRaiderIOButton, "RIGHT", 14, 0)
    self.ui.playerDetailHeader:SetText("Player details")

    local detailScrollFrame = CreateFrame("ScrollFrame", nil, detailView)
    detailScrollFrame:SetPoint("TOPLEFT", 12, -52)
    detailScrollFrame:SetPoint("BOTTOMRIGHT", -20, 12)
    detailScrollFrame:EnableMouseWheel(true)
    self.ui.playerDetailScrollFrame = detailScrollFrame

    local frameW = (self.db and self.db.ui and self.db.ui.width) or 1280
    local detailScrollW = max(760, frameW - 245)
    local detailContent = CreateFrame("Frame", nil, detailScrollFrame)
    detailContent:SetSize(detailScrollW, PLAYER_DETAIL_CONTENT_HEIGHT)
    detailScrollFrame:SetScrollChild(detailContent)
    self.ui.playerDetailContent = detailContent

    local detailScrollBar = CreateFrame("Slider", nil, detailView)
    detailScrollBar:SetOrientation("VERTICAL")
    detailScrollBar:SetWidth(6)
    detailScrollBar:SetPoint("TOPRIGHT", detailView, "TOPRIGHT", -12, -52)
    detailScrollBar:SetPoint("BOTTOMRIGHT", detailView, "BOTTOMRIGHT", -12, 12)
    detailScrollBar:SetMinMaxValues(0, 1)
    detailScrollBar:SetValue(0)
    detailScrollBar:Hide()
    local detailThumb = detailScrollBar:CreateTexture(nil, "ARTWORK")
    detailThumb:SetTexture(WHITE_TEXTURE)
    detailThumb:SetWidth(4)
    detailThumb:SetVertexColor(COLORS.accent[1], COLORS.accent[2], COLORS.accent[3], 0.65)
    detailScrollBar:SetThumbTexture(detailThumb)
    detailScrollBar:SetScript("OnValueChanged", function(_, value)
        detailScrollFrame:SetVerticalScroll(value)
    end)
    detailScrollFrame:SetScript("OnScrollRangeChanged", function(_, _, yRange)
        local range = max(0, yRange or 0)
        detailScrollBar:SetMinMaxValues(0, range)
        detailScrollBar:SetShown(range > 0)
    end)
    detailScrollFrame:SetScript("OnMouseWheel", function(_, delta)
        local current = detailScrollBar:GetValue()
        local _, maxValue = detailScrollBar:GetMinMaxValues()
        detailScrollBar:SetValue(max(0, min(current - delta * 60, maxValue)))
    end)
    self.ui.playerDetailScrollBar = detailScrollBar

    local heroCard = CreateFrame("Frame", nil, detailContent)
    heroCard:SetPoint("TOPLEFT", detailContent, "TOPLEFT", 0, 0)
    heroCard:SetSize(detailScrollW, PLAYER_DETAIL_HERO_HEIGHT)
    self:ApplySurface(heroCard, COLORS.headerBG, COLORS.surface, COLORS.accent)
    self.ui.playerDetailHero = heroCard

    heroCard.Background = heroCard:CreateTexture(nil, "BACKGROUND")
    heroCard.Background:SetAllPoints()
    heroCard.Background:SetAlpha(0.18)

    self.ui.playerDetailPortrait = CreatePortraitWidget(heroCard, 72)
    self.ui.playerDetailPortrait:SetPoint("TOPLEFT", 18, -18)

    self.ui.playerDetailRoleBadge = CreateRoleBadge(heroCard)
    self.ui.playerDetailRoleBadge:SetPoint("TOPRIGHT", -18, -18)

    self.ui.playerDetailName = CreateFont(heroCard, 22, COLORS.text)
    self.ui.playerDetailName:SetPoint("TOPLEFT", self.ui.playerDetailPortrait, "TOPRIGHT", 16, -2)
    self.ui.playerDetailName:SetPoint("TOPRIGHT", heroCard, "TOPRIGHT", -52, -18)

    self.ui.playerDetailRealm = CreateFont(heroCard, 12, COLORS.accentSoft)
    self.ui.playerDetailRealm:SetPoint("TOPLEFT", self.ui.playerDetailName, "BOTTOMLEFT", 0, -6)
    self.ui.playerDetailRealm:SetPoint("TOPRIGHT", heroCard, "TOPRIGHT", -52, -52)
    self.ui.playerDetailRealm:SetJustifyH("LEFT")

    self.ui.playerDetailMeta = CreateFont(heroCard, 11, COLORS.muted)
    self.ui.playerDetailMeta:SetPoint("TOPLEFT", self.ui.playerDetailRealm, "BOTTOMLEFT", 0, -6)
    self.ui.playerDetailMeta:SetPoint("TOPRIGHT", heroCard, "TOPRIGHT", -52, -74)
    self.ui.playerDetailMeta:SetJustifyH("LEFT")

    self.ui.playerDetailSummaryPrimary = CreateFont(heroCard, 11, COLORS.text)
    self.ui.playerDetailSummaryPrimary:SetPoint("TOPLEFT", self.ui.playerDetailMeta, "BOTTOMLEFT", 0, -8)
    self.ui.playerDetailSummaryPrimary:SetPoint("TOPRIGHT", heroCard, "TOPRIGHT", -18, -68)
    self.ui.playerDetailSummaryPrimary:SetJustifyH("LEFT")
    self.ui.playerDetailSummaryPrimary:SetJustifyV("TOP")
    self.ui.playerDetailSummaryPrimary:Hide()

    self.ui.playerDetailSummarySecondary = CreateFont(heroCard, 11, COLORS.muted)
    self.ui.playerDetailSummarySecondary:SetPoint("TOPLEFT", self.ui.playerDetailSummaryPrimary, "BOTTOMLEFT", 0, -6)
    self.ui.playerDetailSummarySecondary:SetPoint("TOPRIGHT", heroCard, "TOPRIGHT", -18, -88)
    self.ui.playerDetailSummarySecondary:SetJustifyH("LEFT")
    self.ui.playerDetailSummarySecondary:SetJustifyV("TOP")
    self.ui.playerDetailSummarySecondary:Hide()

    self.ui.playerDetailHeroNote = CreateFont(heroCard, 10, COLORS.subdued)
    self.ui.playerDetailHeroNote:SetPoint("TOPLEFT", self.ui.playerDetailMeta, "BOTTOMLEFT", 0, -6)
    self.ui.playerDetailHeroNote:SetPoint("TOPRIGHT", heroCard, "TOPRIGHT", -18, -94)
    self.ui.playerDetailHeroNote:SetJustifyH("LEFT")
    self.ui.playerDetailHeroNote:SetJustifyV("TOP")

    local metricSpecs = {
        {key = "runs", label = "Runs"},
        {key = "timed", label = "Timed"},
        {key = "bestKey", label = "Best Key"},
        {key = "avgKey", label = "Avg Key"},
        {key = "maxDps", label = "Max DPS"},
        {key = "avgDps", label = "Avg DPS"},
        {key = "maxHps", label = "Max HPS"},
        {key = "avgHps", label = "Avg HPS"},
    }
    self.ui.playerDetailMetricCards = {}
    local metricGap = 8
    local metricCardW = floor((detailScrollW - 36 - (3 * metricGap)) / 4)
    for index, spec in ipairs(metricSpecs) do
        local card = CreateMetricCard(heroCard, spec.label)
        card:SetSize(metricCardW, 58)
        local column = (index - 1) % 4
        local row = floor((index - 1) / 4)
        card:SetPoint("TOPLEFT", heroCard, "TOPLEFT", 18 + (column * (metricCardW + metricGap)), -118 - (row * 66))
        self.ui.playerDetailMetricCards[spec.key] = card
    end

    self.ui.playerDetailNoteIndicator = CreateFont(heroCard, 10, COLORS.accentSoft, "RIGHT")
    self.ui.playerDetailNoteIndicator:SetPoint("BOTTOMRIGHT", heroCard, "BOTTOMRIGHT", -18, 14)
    self.ui.playerDetailNoteIndicator:Hide()

    local chipsLabel = CreateFont(heroCard, 10, COLORS.subdued)
    chipsLabel:SetPoint("BOTTOMLEFT", heroCard, "BOTTOMLEFT", 108, 14)
    chipsLabel:SetText("")
    chipsLabel:Hide()

    local chipsContainer = CreateFrame("Frame", nil, heroCard)
    chipsContainer:SetPoint("BOTTOMRIGHT", heroCard, "BOTTOMRIGHT", -18, 12)
    chipsContainer:SetSize(0, 26)
    self.ui.playerDetailSpecChipsContainer = chipsContainer

    self.ui.playerDetailSpecChips = {}
    for index = 1, PLAYER_DETAIL_SPEC_CHIPS do
        local chip = CreateSpecChip(chipsContainer)
        chip:Hide()
        self.ui.playerDetailSpecChips[index] = chip
    end

    self.ui.playerDetailSpecChipEmpty = CreateFont(heroCard, 10, COLORS.subdued)
    self.ui.playerDetailSpecChipEmpty:SetPoint("LEFT", chipsContainer, "LEFT", 0, 0)
    self.ui.playerDetailSpecChipEmpty:SetText("")
    self.ui.playerDetailSpecChipEmpty:Hide()

    local noteCard = CreateFrame("Frame", nil, detailContent)
    noteCard:SetPoint("TOPLEFT", heroCard, "BOTTOMLEFT", 0, -12)
    noteCard:SetSize(detailScrollW, PLAYER_DETAIL_NOTE_HEIGHT)
    self:ApplySurface(noteCard, COLORS.sectionBG, COLORS.surface, COLORS.accentSoft)
    self.ui.playerDetailNoteCard = noteCard

    local noteTitle = CreateFont(noteCard, 13, COLORS.text)
    noteTitle:SetPoint("TOPLEFT", 16, -14)
    noteTitle:SetText("Note")

    local noteHint = CreateFont(noteCard, 10, COLORS.subdued)
    noteHint:SetPoint("LEFT", noteTitle, "RIGHT", 10, 0)
    noteHint:SetText("")
    noteHint:Hide()
    self.ui.playerDetailNoteHint = noteHint

    self.ui.playerDetailNoteEdit = CreateActionButton(noteCard, 92, 26, "Add note")
    self.ui.playerDetailNoteEdit:SetPoint("TOPRIGHT", -16, -10)
    self.ui.playerDetailNoteEdit:SetScript("OnClick", function(button)
        MythicTools:ShowPlayerNoteEditor(button.playerName or (MythicTools.db and MythicTools.db.ui and MythicTools.db.ui.selectedPlayer))
    end)

    self.ui.playerDetailNoteText = CreateFont(noteCard, 12, COLORS.text)
    self.ui.playerDetailNoteText:SetPoint("TOPLEFT", 16, -38)
    self.ui.playerDetailNoteText:SetPoint("TOPRIGHT", noteCard, "TOPRIGHT", -124, -38)
    self.ui.playerDetailNoteText:SetPoint("BOTTOMRIGHT", noteCard, "BOTTOMRIGHT", -124, 28)
    self.ui.playerDetailNoteText:SetJustifyH("LEFT")
    self.ui.playerDetailNoteText:SetJustifyV("TOP")
    if self.ui.playerDetailNoteText.SetWordWrap then
        self.ui.playerDetailNoteText:SetWordWrap(true)
    end

    self.ui.playerDetailNoteStatus = CreateFont(noteCard, 10, COLORS.subdued)
    self.ui.playerDetailNoteStatus:SetPoint("BOTTOMLEFT", 16, 12)
    self.ui.playerDetailNoteStatus:SetText("")
    self.ui.playerDetailNoteStatus:Hide()

    local historyCard = CreateFrame("Frame", nil, detailContent)
    historyCard:SetPoint("TOPLEFT", noteCard, "BOTTOMLEFT", 0, -12)
    historyCard:SetSize(detailScrollW, PLAYER_DETAIL_HISTORY_HEIGHT)
    self:ApplySurface(historyCard, COLORS.sectionBG, COLORS.surface, COLORS.accentSoft)
    ClipChildren(historyCard)
    self.ui.playerDetailHistoryCard = historyCard

    local historyTitle = CreateFont(historyCard, 13, COLORS.text)
    historyTitle:SetPoint("TOPLEFT", 16, -14)
    historyTitle:SetText("Last Runs")

    self.ui.playerDetailHistoryCount = CreateFont(historyCard, 11, COLORS.muted, "RIGHT")
    self.ui.playerDetailHistoryCount:SetPoint("TOPRIGHT", -16, -14)

    self.ui.playerDetailSeeAllButton = CreateActionButton(historyCard, 86, 20, "See all")
    self.ui.playerDetailSeeAllButton:SetPoint("TOPRIGHT", historyCard, "TOPRIGHT", -16, -10)
    self.ui.playerDetailSeeAllButton:SetScript("OnClick", function()
        local playerEntry = MythicTools:GetSelectedPlayerEntry()
        if playerEntry then
            local name = playerEntry.shortName or playerEntry.fullName or ""
            MythicTools.db.ui.filters.player = name
            SetAceEditText(MythicTools.ui.runsPlayerFilter, name)
            MythicTools:SetRunViewMode("list")
            MythicTools:ShowTab("Runs", true)
        end
    end)

    local historyBody = CreateFrame("Frame", nil, historyCard)
    historyBody:SetPoint("TOPLEFT", 12, -42)
    historyBody:SetPoint("TOPRIGHT", -12, -42)
    historyBody:SetPoint("BOTTOMLEFT", 12, 12)
    historyBody:SetPoint("BOTTOMRIGHT", -12, 12)
    ClipChildren(historyBody)
    self.ui.playerDetailHistoryBody = historyBody

    self.ui.playerDetailHistoryRows = {}
    for index = 1, PLAYER_DETAIL_HISTORY_ROWS do
        local row = CreatePlayerDetailHistoryRow(historyBody)
        row:SetPoint("TOPLEFT", 0, -((index - 1) * PLAYER_DETAIL_HISTORY_ROW_HEIGHT))
        row:SetPoint("TOPRIGHT", 0, -((index - 1) * PLAYER_DETAIL_HISTORY_ROW_HEIGHT))
        row:SetScript("OnClick", function(button)
            if button.runId then
                MythicTools:ShowCompletionPopup(button.runId)
            end
        end)
        self.ui.playerDetailHistoryRows[index] = row
    end

    self.ui.playerDetailEmptyHistory = CreateFont(historyCard, 12, COLORS.muted, "CENTER")
    self.ui.playerDetailEmptyHistory:SetPoint("CENTER")
    self.ui.playerDetailEmptyHistory:SetText("No runs recorded for this player.")
    self.ui.playerDetailEmptyHistory:Hide()

    return page
end

function MythicTools:BuildSettingsPage(parent)
    local page = CreateFrame("Frame", nil, parent)
    page:SetAllPoints(parent)
    ClipChildren(page)

    local generalCard = CreateFrame("Frame", nil, page)
    generalCard:SetPoint("TOPLEFT", 12, -12)
    generalCard:SetSize(350, 196)
    self:ApplySurface(generalCard, COLORS.sectionBG, COLORS.surface, COLORS.accentSoft)

    local generalTitle = CreateFont(generalCard, 16, COLORS.text)
    generalTitle:SetPoint("TOPLEFT", 16, -16)
    generalTitle:SetText("General")

    self.ui.settingsAlerts = CreateSettingToggle(generalCard, "Chat alerts", "Warn when someone from your history joins your group.", function(checked)
        self:SetChatAlertsEnabled(checked)
        self:RefreshSettingsView()
    end)
    self.ui.settingsAlerts:SetPoint("TOPLEFT", 16, -44)
    self.ui.settingsAlerts:SetPoint("TOPRIGHT", -16, -44)

    self.ui.settingsMinimap = CreateSettingToggle(generalCard, "Minimap button", "Show or hide the circular shortcut around the minimap.", function(checked)
        self:SetMinimapButtonEnabled(checked)
        self:RefreshSettingsView()
    end)
    self.ui.settingsMinimap:SetPoint("TOPLEFT", 16, -92)
    self.ui.settingsMinimap:SetPoint("TOPRIGHT", -16, -92)

    self.ui.settingsLootHistory = CreateSettingToggle(generalCard, "Show loot history", "Display dropped items in the run details and completion popup.", function(checked)
        self.db.settings.showLootHistory = checked and true or false
        self:RefreshAllViews()
    end)
    self.ui.settingsLootHistory:SetPoint("TOPLEFT", 16, -140)
    self.ui.settingsLootHistory:SetPoint("TOPRIGHT", -16, -140)

    local popupCard = CreateFrame("Frame", nil, page)
    popupCard:SetPoint("TOPLEFT", generalCard, "TOPRIGHT", 12, 0)
    popupCard:SetPoint("TOPRIGHT", page, "TOPRIGHT", -12, -12)
    popupCard:SetHeight(242)
    self:ApplySurface(popupCard, COLORS.sectionBG, COLORS.surface, COLORS.accent)

    local popupTitle = CreateFont(popupCard, 16, COLORS.text)
    popupTitle:SetPoint("TOPLEFT", 16, -16)
    popupTitle:SetText("Completion popup")

    self.ui.settingsPopupEnabled = CreateSettingToggle(popupCard, "Open summary automatically", "Show the final window with loot, damage, healing, interrupts, DPS, and deaths.", function(checked)
        self.db.settings.showCompletionPopup = checked and true or false
        if not checked then
            self.runtime.pendingCompletionPopup = nil
            self:HideCompletionPopup()
        end
        self:RefreshSettingsView()
    end)
    self.ui.settingsPopupEnabled:SetPoint("TOPLEFT", 16, -44)
    self.ui.settingsPopupEnabled:SetPoint("TOPRIGHT", -16, -44)

    self.ui.settingsPopupTrigger = CreateCycleControl(popupCard, "Open timing", 190, GetPopupTriggerLabel, function()
        self.db.settings.completionPopupTrigger = GetPopupTriggerNext(self.db.settings.completionPopupTrigger or "LOOT_OPENED")
        self.ui.settingsPopupTrigger:SetValue(self.db.settings.completionPopupTrigger)
        self:RefreshSettingsView()
    end)
    self.ui.settingsPopupTrigger:SetPoint("TOPLEFT", 16, -96)
    self.ui.settingsPopupDelay = CreateLabeledEditBox(popupCard, "Delay (0-10s)", 110, nil)
    self.ui.settingsPopupDelay:SetPoint("LEFT", self.ui.settingsPopupTrigger, "RIGHT", 14, 0)

    local popupDelayApply = CreateActionButton(popupCard, 78, 28, "Save")
    popupDelayApply:SetPoint("LEFT", self.ui.settingsPopupDelay, "RIGHT", 10, -11)
    popupDelayApply:SetScript("OnClick", function()
        self.db.settings.completionPopupDelay = self:Clamp(tonumber(self.ui.settingsPopupDelay.Input:GetText()) or 0, 0, 10)
        self:RefreshSettingsView()
    end)

    self.ui.settingsPopupScale = CreateLabeledEditBox(popupCard, "Scale (0.7-1.4)", 110, nil)
    self.ui.settingsPopupScale:SetPoint("TOPLEFT", 16, -148)

    local popupScaleApply = CreateActionButton(popupCard, 78, 28, "Apply")
    popupScaleApply:SetPoint("LEFT", self.ui.settingsPopupScale, "RIGHT", 10, -11)
    popupScaleApply:SetScript("OnClick", function()
        self.db.settings.completionPopupScale = self:Clamp(tonumber(self.ui.settingsPopupScale.Input:GetText()) or 1, 0.7, 1.4)
        self:ApplyCompletionPopupScale()
        self:RefreshSettingsView()
    end)

    local previewButton = CreateActionButton(popupCard, 160, 28, "Open preview")
    previewButton:SetPoint("TOPLEFT", self.ui.settingsPopupScale, "TOPRIGHT", 102, 0)
    previewButton:SetScript("OnClick", function()
        self:ShowTestCompletionPopup()
    end)

    local historyCard = CreateFrame("Frame", nil, page)
    historyCard:SetPoint("TOPLEFT", 12, -220)
    historyCard:SetSize(350, 184)
    self:ApplySurface(historyCard, COLORS.sectionBG, COLORS.surface, COLORS.accentSoft)

    local historyTitle = CreateFont(historyCard, 16, COLORS.text)
    historyTitle:SetPoint("TOPLEFT", 16, -16)
    historyTitle:SetText("History")

    self.ui.settingsMaxRuns = CreateLabeledEditBox(historyCard, "Saved runs", 120, nil)
    self.ui.settingsMaxRuns:SetPoint("TOPLEFT", 16, -44)

    local applyMaxRuns = CreateActionButton(historyCard, 84, 28, "Apply")
    applyMaxRuns:SetPoint("LEFT", self.ui.settingsMaxRuns, "RIGHT", 10, -11)
    applyMaxRuns:SetScript("OnClick", function()
        self:SetMaxRuns(self.ui.settingsMaxRuns.Input:GetText())
        self:RefreshUI()
    end)

    local clearButton = CreateActionButton(historyCard, 150, 28, "Clear history")
    clearButton:SetPoint("TOPLEFT", 16, -108)
    clearButton:SetScript("OnClick", function()
        StaticPopup_Show("MYTHICTOOLS_CLEAR_HISTORY")
    end)

    local historyNote = CreateFont(historyCard, 10, COLORS.subdued)
    historyNote:SetPoint("TOPLEFT", clearButton, "BOTTOMLEFT", 0, -10)
    historyNote:SetPoint("RIGHT", historyCard, "RIGHT", -16, 0)
    historyNote:SetJustifyH("LEFT")
    historyNote:SetText("History is shared across every character on this account.")

    local summaryCard = CreateFrame("Frame", nil, page)
    summaryCard:SetPoint("TOPLEFT", popupCard, "BOTTOMLEFT", 0, -12)
    summaryCard:SetPoint("BOTTOMRIGHT", page, "BOTTOMRIGHT", -12, 12)
    self:ApplySurface(summaryCard, COLORS.sectionBG, COLORS.surface, COLORS.accentSoft)

    local summaryTitle = CreateFont(summaryCard, 16, COLORS.text)
    summaryTitle:SetPoint("TOPLEFT", 16, -16)
    summaryTitle:SetText("Summary")

    self.ui.settingsSummary = CreateFont(summaryCard, 12, COLORS.muted)
    self.ui.settingsSummary:SetPoint("TOPLEFT", summaryTitle, "BOTTOMLEFT", 0, -14)
    self.ui.settingsSummary:SetPoint("RIGHT", summaryCard, "RIGHT", -16, 0)
    self.ui.settingsSummary:SetJustifyH("LEFT")
    self.ui.settingsSummary:SetJustifyV("TOP")

    if not StaticPopupDialogs.MYTHICTOOLS_CLEAR_HISTORY then
        StaticPopupDialogs.MYTHICTOOLS_CLEAR_HISTORY = {
            text = "Clear every saved Sky Mythic History run?",
            button1 = YES,
            button2 = NO,
            OnAccept = function()
                MythicTools:ClearHistory()
            end,
            timeout = 0,
            whileDead = true,
            hideOnEscape = true,
            preferredIndex = 3,
        }
    end

    if not StaticPopupDialogs.MYTHICTOOLS_DELETE_RUN then
        StaticPopupDialogs.MYTHICTOOLS_DELETE_RUN = {
            text = "Delete this run from Sky Mythic History?\n%s",
            button1 = YES,
            button2 = NO,
            OnAccept = function(_, runId)
                if MythicTools:RemoveRunById(runId) then
                    MythicTools:HideCompletionPopup()
                    MythicTools:SetRunViewMode("list")
                    MythicTools:RefreshAllViews()
                end
            end,
            timeout = 0,
            whileDead = true,
            hideOnEscape = true,
            preferredIndex = 3,
        }
    end

    return page
end

function MythicTools:StoreCompletionPopupPoint()
    if not (self.completionPopup and self.db and self.db.ui) then
        return
    end

    local point, _, relativePoint, xOfs, yOfs = self.completionPopup:GetPoint(1)
    self.db.ui.completionPopupPoint = {point or "CENTER", relativePoint or point or "CENTER", xOfs or 0, yOfs or 0}
end

local function RefreshCompletionSortHeaders(frame)
    if not frame or type(frame.SortHeaders) ~= "table" then
        return
    end

    local sortState = MythicTools.db and MythicTools.db.ui and MythicTools.db.ui.completionPopupSort or nil
    local selectedColumn = sortState and sortState.column or "damage"
    for _, header in ipairs(frame.SortHeaders) do
        local selected = header.columnKey == selectedColumn
        header.Text:SetText(header.label)
        if header.Arrow then
            header.Arrow:SetShown(selected)
            if selected then
                header.Arrow:ClearAllPoints()
                local textWidth = header.Text.GetStringWidth and header.Text:GetStringWidth() or ((#(header.label or "")) * 6)
                local arrowX = min((header:GetWidth() or 0) - 9, textWidth + 4)
                header.Arrow:SetPoint("LEFT", header, "LEFT", max(0, arrowX), 0)
            end
        end
        SetTextColor(header.Text, selected and COLORS.text or COLORS.subdued)
    end
end

local function CreateCompletionSortHeader(parent, label, columnKey, x, width)
    local button = CreateFrame("Button", nil, parent)
    button:SetSize(width, 18)
    button:SetPoint("TOPLEFT", x, -9)
    button:RegisterForClicks("LeftButtonUp")
    button:SetFrameLevel((parent:GetFrameLevel() or 1) + 6)
    button:SetHighlightTexture(WHITE_TEXTURE)
    button.columnKey = columnKey
    button.label = label

    local highlight = button:GetHighlightTexture()
    highlight:SetAllPoints()
    highlight:SetVertexColor(1, 1, 1, 0.08)

    button.Text = CreateFont(button, 11, COLORS.subdued)
    button.Text:SetAllPoints()
    button.Text:SetJustifyH("LEFT")
    button.Text:SetText(label)

    button.Arrow = button:CreateTexture(nil, "OVERLAY")
    button.Arrow:SetSize(9, 12)
    button.Arrow:SetTexture(SORT_ARROW_ICON)
    button.Arrow:SetTexCoord(0.25, 0.75, 0.125, 0.84375)
    button.Arrow:SetBlendMode("BLEND")
    button.Arrow:Hide()

    button:SetScript("OnClick", function(self)
        MythicTools.db.ui.completionPopupSort = {
            column = self.columnKey,
            direction = "desc",
        }
        RefreshCompletionSortHeaders(MythicTools.completionPopup)
        MythicTools:RefreshCompletionPopup()
    end)

    return button
end

local function SetCompletionPlayerText(row, playerName, itemLevel)
    local itemLevelText = FormatItemLevelText(itemLevel)
    row.Name:ClearAllPoints()
    row.ItemLevel:ClearAllPoints()

    if itemLevelText ~= "" then
        row.Name:SetPoint("TOPLEFT", row.PlayerText, "TOPLEFT", 0, -3)
        row.ItemLevel:SetPoint("TOPLEFT", row.Name, "BOTTOMLEFT", 0, -2)
        row.ItemLevel:SetText(("ilvl %s"):format(itemLevelText))
        SetTextColor(row.ItemLevel, COLORS.subdued)
        row.ItemLevel:Show()
    else
        row.Name:SetPoint("CENTER", row.PlayerText, "CENTER", 0, 0)
        row.ItemLevel:SetText("")
        row.ItemLevel:Hide()
    end

    row.Name:SetText(playerName or "")
end

function MythicTools:BuildCompletionPopup()
    if self.completionPopup then
        return self.completionPopup
    end

    local frame = CreateFrame("Frame", "MythicToolsCompletionPopup", UIParent)
    frame:SetSize(1280, COMPLETION_POPUP_HEIGHT)
    frame:SetFrameStrata("FULLSCREEN_DIALOG")
    frame:SetFrameLevel(400)
    frame:SetClampedToScreen(true)
    frame:EnableMouse(true)
    frame:SetToplevel(true)
    frame:Hide()
    self:ApplySurface(frame, COLORS.frameBG, COLORS.frameBorder, COLORS.accent)

    local point = self.db.ui.completionPopupPoint or {"CENTER", "CENTER", 0, 40}
    frame:SetPoint(point[1] or "CENTER", UIParent, point[2] or point[1] or "CENTER", point[3] or 0, point[4] or 40)
    table.insert(UISpecialFrames, frame:GetName())

    local header = CreateFrame("Frame", nil, frame, "BackdropTemplate")
    header:SetPoint("TOPLEFT", 1, -1)
    header:SetPoint("TOPRIGHT", -1, -1)
    header:SetHeight(116)
    header:SetBackdrop({bgFile = WHITE_TEXTURE, edgeFile = WHITE_TEXTURE, edgeSize = 1, insets = {left=1, right=1, top=1, bottom=1}})
    header:SetBackdropColor(COLORS.headerBG[1], COLORS.headerBG[2], COLORS.headerBG[3], 1)
    header:SetBackdropBorderColor(0, 0, 0, 1)

    header.Background = header:CreateTexture(nil, "BACKGROUND", nil, 1)
    header.Background:SetAllPoints()
    header.Background:SetAlpha(0.18)
    self.completionPopupHeroBackground = header.Background

    -- Linha accent na base do header do popup
    local popupHeaderLine = header:CreateTexture(nil, "OVERLAY")
    popupHeaderLine:SetTexture(WHITE_TEXTURE)
    popupHeaderLine:SetPoint("BOTTOMLEFT", 1, 0)
    popupHeaderLine:SetPoint("BOTTOMRIGHT", -1, 0)
    popupHeaderLine:SetHeight(1)
    popupHeaderLine:SetVertexColor(COLORS.accent[1], COLORS.accent[2], COLORS.accent[3], 0.8)

    local iconFrame = CreateFrame("Frame", nil, header)
    iconFrame:SetSize(76, 76)
    iconFrame:SetPoint("TOPLEFT", 18, -18)
    self:ApplySurface(iconFrame, COLORS.frameBG, COLORS.surface, nil)

    iconFrame.Icon = iconFrame:CreateTexture(nil, "ARTWORK")
    iconFrame.Icon:SetPoint("TOPLEFT", 1, -1)
    iconFrame.Icon:SetPoint("BOTTOMRIGHT", -1, 1)
    self.completionPopupHeroIcon = iconFrame.Icon

    frame.Title = CreateFont(header, 22, COLORS.text)
    frame.Title:SetPoint("TOPLEFT", iconFrame, "TOPRIGHT", 16, -2)
    frame.Title:SetPoint("RIGHT", header, "RIGHT", -52, 0)

    frame.Meta = CreateFont(header, 12, COLORS.muted)
    frame.Meta:SetPoint("TOPLEFT", frame.Title, "BOTTOMLEFT", 0, -8)
    frame.Meta:SetPoint("RIGHT", header, "RIGHT", -18, 0)
    frame.Meta:SetJustifyH("LEFT")

    frame.Submeta = CreateFont(header, 11, COLORS.text)
    frame.Submeta:SetPoint("TOPLEFT", frame.Meta, "BOTTOMLEFT", 0, -4)
    frame.Submeta:SetPoint("RIGHT", header, "RIGHT", -18, 0)
    frame.Submeta:SetJustifyH("LEFT")

    frame.Note = CreateFont(header, 10, COLORS.subdued)
    frame.Note:SetPoint("TOPLEFT", frame.Submeta, "BOTTOMLEFT", 0, -5)
    frame.Note:SetPoint("RIGHT", header, "RIGHT", -18, 0)
    frame.Note:SetJustifyH("LEFT")

    frame.Status = CreateFont(header, 14, COLORS.accent, "RIGHT")

    -- Botão fechar estilo Cell no popup
    local closeButton = CreateFrame("Button", nil, header, "BackdropTemplate")
    closeButton:SetSize(20, 20)
    closeButton:SetPoint("TOPRIGHT", -8, -8)
    closeButton:SetBackdrop({bgFile = WHITE_TEXTURE, edgeFile = WHITE_TEXTURE, edgeSize = 1, insets = {left=1, right=1, top=1, bottom=1}})
    closeButton:SetBackdropColor(0.6, 0.1, 0.1, 0.6)
    closeButton:SetBackdropBorderColor(0, 0, 0, 1)
    closeButton:SetScript("OnEnter", function() closeButton:SetBackdropColor(0.6, 0.1, 0.1, 1) end)
    closeButton:SetScript("OnLeave", function() closeButton:SetBackdropColor(0.6, 0.1, 0.1, 0.6) end)
    closeButton:SetScript("OnClick", function()
        MythicTools:HideCompletionPopup()
    end)
    local closeText = closeButton:CreateFontString(nil, "OVERLAY")
    closeText:SetFont(STANDARD_TEXT_FONT, 14, "")
    closeText:SetText("×")
    closeText:SetAllPoints()
    closeText:SetJustifyH("CENTER")
    closeText:SetJustifyV("MIDDLE")
    closeText:SetTextColor(1, 1, 1, 1)
    closeText:SetShadowColor(0, 0, 0)
    closeText:SetShadowOffset(1, -1)

    frame.Status:SetPoint("TOPRIGHT", closeButton, "TOPLEFT", -16, 0)

    frame:SetMovable(true)
    header:EnableMouse(true)
    header:RegisterForDrag("LeftButton")
    header:SetScript("OnDragStart", function()
        frame:StartMoving()
    end)
    header:SetScript("OnDragStop", function()
        frame:StopMovingOrSizing()
        self:StoreCompletionPopupPoint()
    end)

    local statsPanel = CreateFrame("Frame", nil, frame)
    statsPanel:SetPoint("TOPLEFT", header, "BOTTOMLEFT", 14, -14)
    statsPanel:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -14, 14)
    self:ApplySurface(statsPanel, COLORS.panelBG, COLORS.surface, nil)
    ClipChildren(statsPanel)

    local columnName = CreateFont(statsPanel, 11, COLORS.subdued)
    columnName:SetPoint("TOPLEFT", 16, -12)
    columnName:SetText("Player")

    local columnScore = CreateFont(statsPanel, 11, COLORS.subdued)
    columnScore:SetPoint("TOPLEFT", COMPACT_STATS_SCORE_X, -12)
    columnScore:SetText("M+ score")

    local columnItems = CreateFont(statsPanel, 11, COLORS.subdued)
    columnItems:SetPoint("TOPLEFT", COMPACT_STATS_ITEMS_X, -12)
    columnItems:SetText("Loot")

    frame.SortHeaders = {
        CreateCompletionSortHeader(statsPanel, "Damage", "damage", COMPACT_STATS_DAMAGE_X, 68),
        CreateCompletionSortHeader(statsPanel, "DPS", "dps", COMPACT_STATS_DPS_X, 58),
        CreateCompletionSortHeader(statsPanel, "Healing", "healing", COMPACT_STATS_HEALING_X, 68),
        CreateCompletionSortHeader(statsPanel, "HPS", "hps", COMPACT_STATS_HPS_X, 58),
        CreateCompletionSortHeader(statsPanel, "Interrupts", "interrupts", COMPACT_STATS_INTERRUPTS_X, 88),
        CreateCompletionSortHeader(statsPanel, "Dispels", "dispels", 914, 64),
        CreateCompletionSortHeader(statsPanel, "Avoidable", "avoidableDamage", 982, 96),
        CreateCompletionSortHeader(statsPanel, "Deaths", "deaths", 1112, 54),
    }
    RefreshCompletionSortHeaders(frame)

    local rowsBody = CreateFrame("Frame", nil, statsPanel)
    rowsBody:SetPoint("TOPLEFT", 12, -34)
    rowsBody:SetPoint("TOPRIGHT", -14, -34)
    rowsBody:SetHeight(COMPLETION_ROWS * COMPLETION_ROW_HEIGHT)
    ClipChildren(rowsBody)
    frame.rowsBody = rowsBody

    frame.rowsScroll = CreateFrame("ScrollFrame", nil, statsPanel, "FauxScrollFrameTemplate")
    frame.rowsScroll:SetPoint("TOPLEFT", 0, -34)
    frame.rowsScroll:SetPoint("BOTTOMRIGHT", -10, 12)
    frame.rowsScroll:SetScript("OnVerticalScroll", function(scrollFrame, offset)
        FauxScrollFrame_OnVerticalScroll(scrollFrame, offset, COMPLETION_ROW_HEIGHT, function()
            MythicTools:RefreshCompletionPopup()
        end)
    end)
    ApplyThinScrollBar(frame.rowsScroll)

    frame.rows = {}
    for index = 1, COMPLETION_ROWS do
        local row = CreateCompletionStatRow(rowsBody)
        row:SetPoint("TOPLEFT", 0, -((index - 1) * COMPLETION_ROW_HEIGHT))
        row:SetPoint("TOPRIGHT", 0, -((index - 1) * COMPLETION_ROW_HEIGHT))
        frame.rows[index] = row
    end

    local deleteButton = CreateActionButton(statsPanel, 92, 24, "Delete")
    deleteButton:SetPoint("BOTTOMRIGHT", statsPanel, "BOTTOMRIGHT", -12, 8)
    deleteButton:SetScript("OnClick", function()
        local runId = frame and frame.runId
        if runId then
            MythicTools:ConfirmDeleteRun(runId)
        end
    end)
    frame.DeleteButton = deleteButton

    self.completionPopup = frame
    self:ApplyCompletionPopupScale()
    return frame
end

function MythicTools:HideCompletionPopup()
    if self.completionPopup then
        self.completionPopup.runId = nil
        self.completionPopup.previewRun = nil
        self.completionPopup:Hide()
    end
    if self.db and self.db.ui then
        self.db.ui.selectedRunId = nil
    end
    if self.ui then
        self:RefreshRunView()
    end
end

function MythicTools:ShowCompletionPopup(runOrRunId)
    local previewRun = type(runOrRunId) == "table" and runOrRunId or nil
    local runId = previewRun and previewRun.runId or runOrRunId
    if not previewRun and not runId then
        return
    end

    if self.PushRuntimeDebug then
        self:PushRuntimeDebug(("show completion popup run %s preview=%s"):format(
            tostring(runId or (previewRun and previewRun.runId) or "?"),
            tostring(previewRun ~= nil)
        ))
    end

    local frame = self:BuildCompletionPopup()
    frame.previewRun = previewRun
    frame.runId = previewRun and nil or runId
    self:ApplyCompletionPopupScale()
    self:RefreshCompletionPopup()
    frame:Show()
    frame:Raise()
end
function MythicTools:RefreshCompletionPopup()
    local frame = self.completionPopup
    if not frame then
        return
    end

    local run = frame.previewRun or (frame.runId and self:GetRunById(frame.runId)) or nil
    if not run then
        if self.PushRuntimeDebug then
            self:PushRuntimeDebug(("hide completion popup missing run %s"):format(tostring(frame.runId or "?")))
        end
        if self.StoreDebugReport then
            self:StoreDebugReport("popup_missing_run", {
                trigger = "RefreshCompletionPopup",
                runId = frame.runId,
            })
        end
        frame.runId = nil
        frame.previewRun = nil
        frame:Hide()
        return
    end

    if frame.DeleteButton then
        frame.DeleteButton:SetShown(not frame.previewRun and run.runId ~= nil)
    end

    self:SetRunArt(self.completionPopupHeroIcon, self.completionPopupHeroBackground, run)
    frame.Title:SetText(GetCompletionRunTitleText(run))
    frame.Meta:SetText(("%s  •  Time %s  •  Deaths %d"):format(
        self:FormatDateTime(run.endTime),
        self:FormatDurationMS(run.timeMS),
        run.totalDeaths or 0
    ))
    local scoreText = GetRunScoreText(run)
    local submeta = ("%s  •  %s"):format(
        GetRunTimeRemainingText(run),
        GetRunOutOfCombatDisplayText(run)
    )
    if scoreText ~= "" then
        submeta = submeta .. ("  •  %s"):format(scoreText)
    end
    local statsSourceText = GetRunStatsSourceText(run)
    if statsSourceText ~= "" then
        submeta = submeta .. ("  •  %s"):format(statsSourceText)
    end
    frame.Submeta:SetText(submeta)
    frame.Status:SetText("")
    SetTextColor(frame.Status, GetRunStatusColor(run))

    local noteParts = {}
    if run.preview then
        noteParts[#noteParts + 1] = "Preview"
    end
    if run.statsNote and run.statsNote ~= "" then
        noteParts[#noteParts + 1] = run.statsNote
    end
    if self.runtime and self.runtime.lootTracking and run.runId and self.runtime.lootTracking.runId == run.runId then
        local tracking = self.runtime.lootTracking
        if tracking.lootOpened then
            noteParts[#noteParts + 1] = tracking.lootClosed and "Chest loot tracked" or "Tracking chest loot..."
        else
            noteParts[#noteParts + 1] = "Waiting for chest loot..."
        end
    end
    frame.Note:SetText(table.concat(noteParts, "  |  "))
    SetTextColor(frame.Note, run.statsUnavailable and COLORS.danger or COLORS.subdued)

    RefreshCompletionSortHeaders(frame)
    local sortedStats = self:GetSortedStatsForRun(run)
    local leaders = GetCompletionStatLeaders(self, run, sortedStats)
    if frame.rowsScroll then
        FauxScrollFrame_Update(frame.rowsScroll, #sortedStats, COMPLETION_ROWS, COMPLETION_ROW_HEIGHT)
    end
    local offset = frame.rowsScroll and FauxScrollFrame_GetOffset(frame.rowsScroll) or 0
    for index, row in ipairs(frame.rows) do
        local stat = sortedStats[offset + index]
        if stat then
            row:Show()
            if row.PlayerButton then
                row.PlayerButton.playerName = stat.name
            end
            self:SetPortraitWidget(row.Portrait, stat.name, stat.classFilename, stat.specIconID, stat.specID)

            local classColor = stat.classFilename and RAID_CLASS_COLORS and RAID_CLASS_COLORS[stat.classFilename]
            if classColor then
                row.Name:SetTextColor(classColor.r, classColor.g, classColor.b, 1)
            else
                SetTextColor(row.Name, COLORS.text)
            end

            SetCompletionPlayerText(row, stat.shortName or self:GetShortName(stat.name or ""), stat.itemLevel)
            row.Score:SetText(GetPlayerScoreDisplayText(stat))
            self:SetLootButtons(row.LootButtons, row.LootLabel, row.LootOverflow, row.LootEmpty, stat.loot, row.LootTextButtons)
            local dpsValue = self:GetPlayerDPS(run, stat)
            local hpsValue = self:GetPlayerHPS(run, stat)
            local interruptsValue = tonumber(stat.interrupts) or 0
            local dispelsValue = tonumber(stat.dispels) or 0
            local avoidableDamageValue = stat.avoidableDamage ~= nil and (tonumber(stat.avoidableDamage) or 0) or nil
            row.Damage:SetText(self:FormatAmount(stat.damage))
            row.DPS:SetText(self:FormatAmount(dpsValue))
            row.Healing:SetText(self:FormatAmount(stat.healing))
            row.HPS:SetText(self:FormatAmount(hpsValue))
            row.Interrupts:SetText(tostring(interruptsValue))
            row.Dispels:SetText(tostring(dispelsValue))
            row.AvoidableDamage:SetText(self:FormatAmount(avoidableDamageValue))
            row.Deaths:SetText(tostring(stat.deaths or 0))
            SetTextColor(row.Score, GetPlayerScoreColor(stat))
            SetTextColor(row.DPS, IsCompletionStatLeader(dpsValue, leaders.dps) and COLORS.bestMetric or COLORS.text)
            SetTextColor(row.HPS, IsCompletionStatLeader(hpsValue, leaders.hps) and COLORS.bestMetric or COLORS.text)
            SetTextColor(row.Interrupts, IsCompletionStatLeader(interruptsValue, leaders.interrupts) and COLORS.bestMetric or COLORS.text)
            SetTextColor(row.Dispels, IsCompletionStatLeader(dispelsValue, leaders.dispels) and COLORS.bestMetric or COLORS.text)
            SetTextColor(row.AvoidableDamage, (avoidableDamageValue ~= nil and leaders.avoidableDamage ~= nil and avoidableDamageValue == leaders.avoidableDamage) and COLORS.bestMetric or COLORS.text)
            SetTextColor(row.Deaths, (stat.deaths or 0) > 0 and COLORS.danger or COLORS.text)
            if row.DPSHover then
                row.DPSHover.playerStat = stat
                row.DPSHover:Show()
            end
            if row.HPSHover then
                row.HPSHover.playerStat = stat
                row.HPSHover:Show()
            end
            if row.AvoidableHover then
                row.AvoidableHover.playerStat = stat
                row.AvoidableHover:Show()
            end
        else
            if row.PlayerButton then
                row.PlayerButton.playerName = nil
            end
            SetCompletionPlayerText(row, "", nil)
            row.Score:SetText("")
            row.Damage:SetText("")
            row.DPS:SetText("")
            row.Healing:SetText("")
            row.HPS:SetText("")
            row.Interrupts:SetText("")
            row.Dispels:SetText("")
            row.AvoidableDamage:SetText("")
            row.Deaths:SetText("")
            self:ResetLootButtons(row.LootButtons, row.LootLabel, row.LootOverflow, row.LootEmpty, row.LootTextButtons)
            if row.DPSHover then
                row.DPSHover.playerStat = nil
                row.DPSHover:Hide()
            end
            if row.HPSHover then
                row.HPSHover.playerStat = nil
                row.HPSHover:Hide()
            end
            if row.AvoidableHover then
                row.AvoidableHover.playerStat = nil
                row.AvoidableHover:Hide()
            end
            row:Hide()
        end
    end
end

function MythicTools:UpdateMinimapButtonPosition()
    if not (self.minimapButton and Minimap and self.db and self.db.settings) then
        return
    end

    local angle = tonumber(self.db.settings.minimapAngle) or -35
    local radius = GetMinimapButtonRadius()
    self.minimapButton:ClearAllPoints()
    self.minimapButton:SetPoint("CENTER", Minimap, "CENTER", cos(rad(angle)) * radius, sin(rad(angle)) * radius)
end

function MythicTools:CreateMinimapButton()
    if self.minimapButton or not Minimap then
        return self.minimapButton
    end

    local button = CreateFrame("Button", "MythicToolsMinimapButton", Minimap)
    button:SetSize(MINIMAP_BUTTON_SIZE, MINIMAP_BUTTON_SIZE)
    button:SetFrameStrata("MEDIUM")
    button:SetFrameLevel((Minimap:GetFrameLevel() or 0) + 8)
    button:RegisterForClicks("LeftButtonUp", "RightButtonUp")
    button:RegisterForDrag("LeftButton")

    button.Border = button:CreateTexture(nil, "OVERLAY")
    button.Border:SetTexture("Interface\\Minimap\\MiniMap-TrackingBorder")
    button.Border:SetSize(54, 54)
    button.Border:SetPoint("TOPLEFT")

    button.Bg = button:CreateTexture(nil, "BACKGROUND")
    button.Bg:SetTexture("Interface\\Minimap\\TRACKINGBACKGROUND")
    button.Bg:SetPoint("TOPLEFT", 6, -6)
    button.Bg:SetPoint("BOTTOMRIGHT", -6, 6)
    button.Bg:SetTexCoord(0.08, 0.92, 0.08, 0.92)
    button.Bg:SetVertexColor(0.18, 0.28, 0.34, 1)

    button.Icon = button:CreateTexture(nil, "ARTWORK")
    button.Icon:SetPoint("TOPLEFT", MINIMAP_BUTTON_ICON_INSET, -MINIMAP_BUTTON_ICON_INSET)
    button.Icon:SetPoint("BOTTOMRIGHT", -MINIMAP_BUTTON_ICON_INSET, MINIMAP_BUTTON_ICON_INSET)
    button.Icon:SetTexture(ADDON_ICON)
    button.Icon:SetBlendMode("ADD")
    button.Icon:SetTexCoord(0, 1, 0, 1)

    button:SetHighlightTexture(WHITE_TEXTURE)
    local highlight = button:GetHighlightTexture()
    highlight:SetPoint("TOPLEFT", MINIMAP_BUTTON_ICON_INSET, -MINIMAP_BUTTON_ICON_INSET)
    highlight:SetPoint("BOTTOMRIGHT", -MINIMAP_BUTTON_ICON_INSET, MINIMAP_BUTTON_ICON_INSET)
    highlight:SetVertexColor(1, 1, 1, 0.10)

    button:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, "ANCHOR_LEFT")
        GameTooltip:AddLine(ADDON_TITLE_TEXT, 1, 0.82, 0)
        GameTooltip:AddLine("Left click: open history", 1, 1, 1)
        GameTooltip:AddLine("Right click: open settings", 1, 1, 1)
        GameTooltip:Show()
    end)

    button:SetScript("OnLeave", function()
        GameTooltip:Hide()
    end)

    button:SetScript("OnClick", function(_, mouseButton)
        if mouseButton == "RightButton" then
            MythicTools:ToggleMainFrame(true)
            MythicTools:ShowTab("Settings")
        else
            MythicTools:ToggleMainFrame()
        end
    end)

    button:SetScript("OnDragStart", function(self)
        GameTooltip:Hide()
        self:SetScript("OnUpdate", function()
            local centerX, centerY = Minimap:GetCenter()
            local scale = Minimap:GetEffectiveScale() or 1
            local cursorX, cursorY = GetCursorPosition()
            cursorX = cursorX / scale
            cursorY = cursorY / scale
            MythicTools.db.settings.minimapAngle = deg(atan2(cursorY - centerY, cursorX - centerX))
            MythicTools:UpdateMinimapButtonPosition()
        end)
    end)

    button:SetScript("OnDragStop", function(self)
        self:SetScript("OnUpdate", nil)
        MythicTools:UpdateMinimapButtonPosition()
    end)

    self.minimapButton = button
    return button
end

function MythicTools:UpdateMinimapButton()
    if not (self.db and self.db.settings) then
        return
    end

    local button = self:CreateMinimapButton()
    if not button then
        return
    end

    if self.db.settings.showMinimapButton then
        self:UpdateMinimapButtonPosition()
        button:Show()
    else
        button:Hide()
    end
end
function MythicTools:BuildUI()
    if self.ui then
        return self.ui
    end

    local ui = {}
    self.ui = ui

    local frame = CreateFrame("Frame", "MythicToolsMainFrame", UIParent)
    frame:SetSize(self.db.ui.width, self.db.ui.height)
    frame:SetFrameStrata("DIALOG")
    frame:SetClampedToScreen(true)
    frame:Hide()
    self:ApplySurface(frame, COLORS.frameBG, COLORS.frameBorder, COLORS.accent)

    frame.Background = frame:CreateTexture(nil, "BACKGROUND")
    frame.Background:SetAllPoints()
    frame.Background:SetTexture(DEFAULT_DUNGEON_BG)
    frame.Background:SetAlpha(0.06)

    local point = self.db.ui.point or {"CENTER", "CENTER", 0, 0}
    frame:SetPoint(point[1] or "CENTER", UIParent, point[2] or point[1] or "CENTER", point[3] or 0, point[4] or 0)
    table.insert(UISpecialFrames, frame:GetName())
    ui.frame = frame

    -- Header: estilo Cell — barra escura compacta com título na cor de classe
    local header = CreateFrame("Frame", nil, frame, "BackdropTemplate")
    header:SetPoint("TOPLEFT", 1, -1)
    header:SetPoint("TOPRIGHT", -1, -1)
    header:SetHeight(36)
    header:SetBackdrop({bgFile = WHITE_TEXTURE, edgeFile = WHITE_TEXTURE, edgeSize = 1, insets = {left=1, right=1, top=1, bottom=1}})
    header:SetBackdropColor(COLORS.headerBG[1], COLORS.headerBG[2], COLORS.headerBG[3], 1)
    header:SetBackdropBorderColor(0, 0, 0, 1)

    -- Linha de destaque na cor de classe na base do header
    local headerAccentLine = header:CreateTexture(nil, "OVERLAY")
    headerAccentLine:SetTexture(WHITE_TEXTURE)
    headerAccentLine:SetPoint("BOTTOMLEFT", 1, 0)
    headerAccentLine:SetPoint("BOTTOMRIGHT", -1, 0)
    headerAccentLine:SetHeight(1)
    headerAccentLine:SetVertexColor(COLORS.accent[1], COLORS.accent[2], COLORS.accent[3], 0.8)

    local titleIcon = header:CreateTexture(nil, "OVERLAY", nil, 7)
    titleIcon:SetTexture(ADDON_ICON)
    titleIcon:SetBlendMode("ADD")
    titleIcon:SetTexCoord(0, 1, 0, 1)
    titleIcon:SetSize(16, 16)
    titleIcon:SetPoint("LEFT", 14, 0)

    local title = CreateFont(header, 13, COLORS.accent)
    title:SetPoint("LEFT", titleIcon, "RIGHT", 6, 0)
    title:SetText(ADDON_TITLE_TEXT)

    -- Botão fechar estilo Cell (× vermelho)
    local closeButton = CreateFrame("Button", nil, header, "BackdropTemplate")
    closeButton:SetSize(20, 20)
    closeButton:SetPoint("RIGHT", -6, 0)
    closeButton:SetBackdrop({bgFile = WHITE_TEXTURE, edgeFile = WHITE_TEXTURE, edgeSize = 1, insets = {left=1, right=1, top=1, bottom=1}})
    closeButton:SetBackdropColor(0.6, 0.1, 0.1, 0.6)
    closeButton:SetBackdropBorderColor(0, 0, 0, 1)
    closeButton:SetScript("OnEnter", function() closeButton:SetBackdropColor(0.6, 0.1, 0.1, 1) end)
    closeButton:SetScript("OnLeave", function() closeButton:SetBackdropColor(0.6, 0.1, 0.1, 0.6) end)
    closeButton:SetScript("OnClick", function()
        MythicTools:HideCompletionPopup()
        frame:Hide()
    end)
    local closeText = closeButton:CreateFontString(nil, "OVERLAY")
    closeText:SetFont(STANDARD_TEXT_FONT, 14, "")
    closeText:SetText("×")
    closeText:SetAllPoints()
    closeText:SetJustifyH("CENTER")
    closeText:SetJustifyV("MIDDLE")
    closeText:SetTextColor(1, 1, 1, 1)
    closeText:SetShadowColor(0, 0, 0)
    closeText:SetShadowOffset(1, -1)

    frame:SetMovable(true)
    header:EnableMouse(true)
    header:RegisterForDrag("LeftButton")
    header:SetScript("OnDragStart", function()
        frame:StartMoving()
    end)
    header:SetScript("OnDragStop", function()
        frame:StopMovingOrSizing()
        self:StoreMainFramePoint()
    end)

    -- Sidebar estilo Cell
    local sidebar = CreateFrame("Frame", nil, frame)
    sidebar:SetPoint("TOPLEFT", header, "BOTTOMLEFT", 0, -8)
    sidebar:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 1, 1)
    sidebar:SetWidth(204)
    self:ApplySurface(sidebar, COLORS.panelBG, COLORS.surface, COLORS.accentSoft)

    -- Separador de navegação estilo Cell
    local sidebarTitle = CreateFont(sidebar, 11, COLORS.accent)
    sidebarTitle:SetPoint("TOPLEFT", 14, -14)
    sidebarTitle:SetText("Navigation")
    local sidebarTitleLine = sidebar:CreateTexture(nil, "ARTWORK")
    sidebarTitleLine:SetTexture(WHITE_TEXTURE)
    sidebarTitleLine:SetPoint("TOPLEFT", sidebarTitle, "BOTTOMLEFT", 0, -3)
    sidebarTitleLine:SetWidth(176)
    sidebarTitleLine:SetHeight(1)
    sidebarTitleLine:SetVertexColor(COLORS.accent[1], COLORS.accent[2], COLORS.accent[3], 0.5)

    ui.tabButtons = {}
    for index, tabName in ipairs({"Runs", "Players", "Stats", "Settings"}) do
        local button = CreateActionButton(sidebar, 176, 30, tabName)
        button:SetPoint("TOPLEFT", 14, -34 - ((index - 1) * 38))
        button:SetScript("OnClick", function()
            if tabName == "Players" then
                self.db.ui.playersView = "index"
                self.db.ui.selectedPlayer = nil
            end
            self:ShowTab(tabName)
        end)
        ui.tabButtons[tabName] = button
    end

    ui.sidebarRuns = CreateSidebarMetric(sidebar, "Saved runs")
    ui.sidebarRuns:SetPoint("TOPLEFT", 14, -200)

    ui.sidebarPlayers = CreateSidebarMetric(sidebar, "Indexed players")
    ui.sidebarPlayers:SetPoint("TOPLEFT", 14, -262)

    ui.sidebarStatus = CreateSidebarMetric(sidebar, "No active run")
    ui.sidebarStatus:SetPoint("TOPLEFT", 14, -324)

    local footer = CreateFont(sidebar, 10, COLORS.subdued)
    footer:SetPoint("BOTTOMLEFT", 14, 12)
    footer:SetText(("v%s"):format(self.version or "0.3.0"))

    local content = CreateFrame("Frame", nil, frame)
    content:SetPoint("TOPLEFT", sidebar, "TOPRIGHT", 8, 0)
    content:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -1, 1)
    self:ApplySurface(content, COLORS.panelBG, COLORS.surface, nil)
    ClipChildren(content)

    ui.pages = {}
    ui.pages.Runs = self:BuildRunsPage(content)
    ui.pages.Players = self:BuildPlayersPage(content)
    ui.pages.Stats = self:BuildStatsPage(content)
    ui.pages.Settings = self:BuildSettingsPage(content)

    for pageName, page in pairs(ui.pages) do
        page:SetShown(pageName == (self.db.ui.activeTab or "Runs"))
    end

    frame:SetScript("OnShow", function()
        self:RefreshUI()
    end)

    self:SyncFilterWidgets()

    if self.UpdateMinimapButton then
        self:UpdateMinimapButton()
    end
    if self.InitializeUnitPopupMenu then
        self:InitializeUnitPopupMenu()
    end

    return ui
end
