-- MythicTrackerSeasons.lua
-- Full-featured Mythic+ tracking addon for WoW Midnight
-- Tracks: timed/depleted runs, keystone level, affixes, dates — per character/season
-- SavedVariables: MythicTrackerSeasonDB

local ADDON_NAME = "MythicTracker"
local DB_VERSION = 2
local MIN_LEVEL  = 90

-- Midnight Season 1 M+ starts March 24 2026 00:00 UTC
local SEASON_START_TIMESTAMP = 1742774400

-- Weekly ID: increments every Tuesday 00:00 UTC (epoch Jan 1 1970 = Thursday, -2 days aligns to Tuesday)
local function GetWeekID()
    return math.floor((time() - 172800) / 604800)
end

local function GetWeeklyRuns(charKey)
    local cdb = MythicTrackerSeasonDB and MythicTrackerSeasonDB[charKey or GetCharKey()]
    if not cdb or not cdb.weekly then return 0 end
    return cdb.weekly[GetWeekID()] or 0
end

local function GetVaultSlotsFromRuns(runs)
    if runs >= 8 then return 3
    elseif runs >= 4 then return 2
    elseif runs >= 1 then return 1
    else return 0 end
end

local DUNGEON_NAMES = {
    "Algeth'ar Academy",
    "Magisters' Terrace",
    "Maisara Caverns",
    "Nexus-Point Xenas",
    "Pit of Saron",
    "Seat of the Triumvirate",
    "Skyreach",
    "Windrunner Spire",
}

-- ─── Helpers ─────────────────────────────────────────────────────────────────

local function GetCurrentSeason()
    if C_MythicPlus and C_MythicPlus.GetCurrentSeason then
        local s = C_MythicPlus.GetCurrentSeason()
        if s and s > 0 then return s end
    end
    return 1
end
MTS_GetCurrentSeason = GetCurrentSeason  -- exposed for MythicTrackerMock.lua

local function GetCharKey()
    return (UnitName("player") or "Unknown") .. "-" .. (GetRealmName() or "Unknown")
end
MTS_GetCharKey = GetCharKey  -- exposed for MythicTrackerMock.lua

local function IsEligibleLevel()
    return (UnitLevel("player") or 0) >= MIN_LEVEL
end

local function IsSeasonActive()
    if C_MythicPlus and C_MythicPlus.GetCurrentSeason then
        local s = C_MythicPlus.GetCurrentSeason()
        if s and s > 0 then return true end
    end
    return time() >= SEASON_START_TIMESTAMP
end

local function StripColors(s)
    if not s then return nil end
    return s:gsub("|c%x%x%x%x%x%x%x%x",""):gsub("|r",""):match("^%s*(.-)%s*$")
end

local function FormatTimeAgo(ts)
    if not ts then return "Never" end
    local diff = time() - ts
    if diff < 60       then return "Just now"
    elseif diff < 3600 then return string.format("%dm ago", math.floor(diff/60))
    elseif diff < 86400 then return string.format("%dh ago", math.floor(diff/3600))
    elseif diff < 604800 then return string.format("%dd ago", math.floor(diff/86400))
    else return date("%b %d", ts)
    end
end

local function GetTimingColor(rate)
    -- rate is 0-100
    if rate >= 80 then return 0.2, 0.85, 0.3, 1   -- green
    elseif rate >= 50 then return 1, 0.82, 0, 1    -- gold
    else return 0.9, 0.25, 0.25, 1                  -- red
    end
end

local function GetCurrentAffixes()
    local names = {}
    if C_MythicPlus and C_MythicPlus.GetCurrentAffixes then
        local affixes = C_MythicPlus.GetCurrentAffixes()
        if affixes then
            for _, a in ipairs(affixes) do
                local name = a.name or (C_ChallengeMode and C_ChallengeMode.GetAffixInfo and select(1, C_ChallengeMode.GetAffixInfo(a.id)))
                if name then table.insert(names, name) end
            end
        end
    end
    return names
end

local function FormatRunTime(ms)
    if not ms or ms <= 0 then return "--:--" end
    local s = math.floor(ms / 1000)
    return string.format("%d:%02d", math.floor(s/60), s%60)
end

local function FormatDateTime(ts)
    if not ts then return "Unknown" end
    return date("%b %d  %H:%M", ts)
end

local function GetPlayerScore(unit)
    local score = 0
    pcall(function()
        if C_PlayerInfo and C_PlayerInfo.GetPlayerMythicPlusRatingSummary then
            local s = C_PlayerInfo.GetPlayerMythicPlusRatingSummary(unit)
            if s then score = s.currentSeasonScore or 0 end
        end
    end)
    return score
end

local function CaptureGroupSnapshot()
    local members = {}
    local units = {"player","party1","party2","party3","party4"}
    for _, unit in ipairs(units) do
        if UnitExists(unit) then
            local name  = UnitName(unit) or "Unknown"
            local _, classFile = UnitClass(unit)
            local _, className = UnitClass(unit)
            local spec  = ""
            local score = GetPlayerScore(unit)
            pcall(function()
                if unit == "player" then
                    local sid = GetSpecialization()
                    if sid then
                        local _, sname = GetSpecializationInfo(sid)
                        spec = sname or ""
                    end
                else
                    local sid = GetInspectSpecialization and GetInspectSpecialization(unit)
                    if sid and sid > 0 then
                        local _, sname = GetSpecializationInfoByID(sid)
                        spec = sname or ""
                    end
                end
            end)
            table.insert(members, {
                name      = name,
                class     = classFile  or "WARRIOR",
                className = className  or "Unknown",
                spec      = spec,
                score     = score,
            })
        end
    end
    return members
end

-- ─── DB ──────────────────────────────────────────────────────────────────────
-- Structure:
-- MythicTrackerSeasonDB = {
--   version = N,
--   panelX, panelY,         -- saved panel position
--   minimapAngle,           -- minimap button angle
--   [charKey] = {
--     name, level, realm,
--     [seasonStr] = {
--       [dungeonName] = {
--         timed=N, depleted=N,
--         bestKey=N,          -- highest timed keystone level
--         lastRun=timestamp,
--         lastRunTimed=bool,
--         affixes = {
--           [affixName] = { timed=N, depleted=N }
--         }
--       }
--     }
--   }
-- }

local function EnsureDB()
    MythicTrackerSeasonDB = MythicTrackerSeasonDB or { version = DB_VERSION }
end

local function GetCharDB(charKey)
    EnsureDB()
    charKey = charKey or GetCharKey()
    if not MythicTrackerSeasonDB[charKey] then
        MythicTrackerSeasonDB[charKey] = {}
    end
    local cdb = MythicTrackerSeasonDB[charKey]
    if charKey == GetCharKey() then
        cdb.name  = UnitName("player") or charKey
        cdb.level = UnitLevel("player") or 0
        cdb.realm = GetRealmName() or ""
        local className, classFile = UnitClass("player")
        cdb.class     = classFile
        cdb.className = className
        local specIndex = GetSpecialization and GetSpecialization()
        if specIndex then
            local _, specName = GetSpecializationInfo(specIndex)
            cdb.spec = specName
        end
        -- Store equipped item level
        pcall(function()
            local equipped, _ = GetAverageItemLevel()
            if equipped and equipped > 0 then
                cdb.ilvl = math.floor(equipped)
            end
        end)
    end
    return cdb
end

local function GetSeasonDB(charKey, season)
    local cdb = GetCharDB(charKey)
    season = tostring(season or GetCurrentSeason())
    cdb[season] = cdb[season] or {}
    return cdb[season]
end

local function RecordRun(dungeonName, wasOnTime, keystoneLevel, affixNames)
    if not IsEligibleLevel() then return end
    if not dungeonName then return end
    local db = GetSeasonDB(GetCharKey(), GetCurrentSeason())
    db[dungeonName] = db[dungeonName] or { timed=0, depleted=0, bestKey=0, affixes={} }
    local d = db[dungeonName]

    if wasOnTime then
        d.timed   = (d.timed or 0) + 1
        if keystoneLevel and keystoneLevel > (d.bestKey or 0) then
            d.bestKey = keystoneLevel
        end
    else
        d.depleted = (d.depleted or 0) + 1
    end

    d.lastRun      = time()
    d.lastRunTimed = wasOnTime

    -- Track weekly run count (resets each Tuesday)
    local cdb2 = GetCharDB(GetCharKey())
    cdb2.weekly = cdb2.weekly or {}
    local wid = GetWeekID()
    cdb2.weekly[wid] = (cdb2.weekly[wid] or 0) + 1

    -- Append to run history log
    cdb2.runHistory = cdb2.runHistory or {}
    table.insert(cdb2.runHistory, {
        dungeon   = dungeonName,
        level     = keystoneLevel or 0,
        timed     = wasOnTime,
        timestamp = time(),
        affixes   = affixNames or {},
        group     = pendingGroup or {},
    })
    -- Keep last 200 runs per character
    while #cdb2.runHistory > 200 do
        table.remove(cdb2.runHistory, 1)
    end

    -- Track per-affix performance
    if affixNames then
        d.affixes = d.affixes or {}
        for _, name in ipairs(affixNames) do
            d.affixes[name] = d.affixes[name] or { timed=0, depleted=0 }
            if wasOnTime then
                d.affixes[name].timed = (d.affixes[name].timed or 0) + 1
            else
                d.affixes[name].depleted = (d.affixes[name].depleted or 0) + 1
            end
        end
    end
end

-- ─── Challenge Mode ───────────────────────────────────────────────────────────

local pendingDungeon, pendingLevel, pendingAffixes, pendingGroup
local lastInstanceName   = nil
local lastKeystoneLevel  = nil
local lastCompletedLevel = nil

local function ResolveDungeonName()
    -- GetInstanceInfo() is the most reliable source while inside an instance
    local iName
    pcall(function()
        iName = GetInstanceInfo and select(1, GetInstanceInfo())
    end)
    if iName and iName ~= "" and iName ~= "Unknown" then return iName end

    if C_ChallengeMode and C_ChallengeMode.GetActiveChallengeMapID then
        local mapID = C_ChallengeMode.GetActiveChallengeMapID()
        if mapID and mapID > 0 and C_ChallengeMode.GetMapUIInfo then
            local name = C_ChallengeMode.GetMapUIInfo(mapID)
            if name and name ~= "" then return name end
        end
    end

    if lastInstanceName and lastInstanceName ~= "" then return lastInstanceName end
    return GetRealZoneText and GetRealZoneText() or nil
end

local function GetActiveKeystoneLevel()
    if C_ChallengeMode and C_ChallengeMode.GetActiveKeystoneInfo then
        local level = C_ChallengeMode.GetActiveKeystoneInfo()
        if level and level > 0 then return level end
    end
    return nil
end

-- ─── Vault Progress ──────────────────────────────────────────────────────────

local function GetVaultMythicProgress()
    -- Returns: slotsComplete (0-3), currentProgress, nextThreshold
    -- Uses C_WeeklyRewards if available, otherwise returns 0
    if not C_WeeklyRewards or not C_WeeklyRewards.GetActivities then
        return 0, 0, 1
    end
    local typeID = (Enum.WeeklyRewardChestThresholdType and
                    Enum.WeeklyRewardChestThresholdType.MythicPlus) or 3
    local ok, acts = pcall(C_WeeklyRewards.GetActivities, typeID)
    if not ok or not acts then return 0, 0, 1 end

    local slotsComplete = 0
    local progress, threshold = 0, 1
    for _, act in ipairs(acts) do
        if (act.progress or 0) >= (act.threshold or 1) then
            slotsComplete = slotsComplete + 1
        else
            progress  = act.progress  or 0
            threshold = act.threshold or 1
            break
        end
    end
    return slotsComplete, progress, threshold
end

-- ─── Panel State ─────────────────────────────────────────────────────────────

local panel, seasonBtn, minimapBtn
local viewMode    = "individual"
local viewSeason  = nil
local selectedAltKey = nil  -- which alt is shown in global view
local activeTab   = "mplus"   -- "mplus" or "lfg"
local rowPool     = {}

-- ─── LFG DB ──────────────────────────────────────────────────────────────────

local function GetLFGDB(charKey)
    local cdb = GetCharDB(charKey)
    cdb.lfg = cdb.lfg or {
        invited=0, declined=0,
        dungeons={},   -- [dungeonName] = {invited=N, declined=N}
    }
    return cdb.lfg
end

local function RecordLFG(status, activityName)
    if not IsEligibleLevel() then return end
    local db = GetLFGDB(GetCharKey())
    if status == "declined" then
        db.declined = (db.declined or 0) + 1
    elseif status == "invited" then
        db.invited = (db.invited or 0) + 1
    else
        return
    end
    -- Match activity name against known dungeons (exact first, then substring)
    if activityName then
        local needle = activityName:lower()
        for _, dname in ipairs(DUNGEON_NAMES) do
            if dname:lower() == needle or needle:find(dname:lower(), 1, true) then
                db.dungeons[dname] = db.dungeons[dname] or { invited=0, declined=0 }
                db.dungeons[dname][status] = (db.dungeons[dname][status] or 0) + 1
                break
            end
        end
    end
end

-- ─── Row Pool ────────────────────────────────────────────────────────────────

local function ClearRows()
    for _, obj in ipairs(rowPool) do
        obj:Hide()
        if obj.SetScript then
            obj:SetScript("OnEnter", nil)
            obj:SetScript("OnLeave", nil)
        end
    end
    rowPool = {}
end

local function PT(parent, size, r, g, b, text, x, y)
    local fs = parent:CreateFontString(nil, "OVERLAY")
    fs:SetFont("Fonts\\FRIZQT__.TTF", size, "OUTLINE")
    fs:SetTextColor(r, g, b, 1)
    fs:SetJustifyH("LEFT")
    fs:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)
    fs:SetText(text)
    table.insert(rowPool, fs)
    return fs
end

local function HSep(parent, r, g, b, a, h, x, y, w)
    local t = parent:CreateTexture(nil, "ARTWORK")
    t:SetColorTexture(r, g, b, a)
    t:SetSize(w or 700, h or 1)
    t:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)
    table.insert(rowPool, t)
    return t
end

-- ─── Summary Bar ─────────────────────────────────────────────────────────────

local function GetDungeonStats(charKey, season)
    -- Returns table of { name, timed, depleted, bestKey, lastRun, rate }
    local sdb = GetSeasonDB(charKey, season)
    local stats = {}
    for _, name in ipairs(DUNGEON_NAMES) do
        local e = sdb[name] or {}
        local t = e.timed    or 0
        local d = e.depleted or 0
        local total = t + d
        local rate  = total > 0 and math.floor((t/total)*100+0.5) or 0
        table.insert(stats, {
            name=name, timed=t, depleted=d,
            bestKey=e.bestKey or 0,
            lastRun=e.lastRun,
            lastRunTimed=e.lastRunTimed,
            rate=rate, total=total,
            affixes=e.affixes or {}
        })
    end
    return stats
end

local function DrawSummaryBar(sc, charKey, season, y)
    local stats = GetDungeonStats(charKey, season)
    local best, worst = nil, nil
    for _, s in ipairs(stats) do
        if s.total > 0 then
            if not best  or s.rate > best.rate  then best  = s end
            if not worst or s.rate < worst.rate then worst = s end
        end
    end

    HSep(sc, 0.5, 0.4, 0.1, 0.6, 1, 12, y, 700)
    y = y - 14

    if best and worst then
        local br, bg, bb = GetTimingColor(best.rate)
        local wr, wg, wb = GetTimingColor(worst.rate)
        PT(sc, 11, 0.7, 0.7, 0.7, "Best:", 12, y)
        PT(sc, 11, br, bg, bb,
            string.format("%s (%d%%)", best.name, best.rate), 48, y)
        PT(sc, 11, 0.7, 0.7, 0.7, "Worst:", 280, y)
        PT(sc, 11, wr, wg, wb,
            string.format("%s (%d%%)", worst.name, worst.rate), 320, y)
    else
        PT(sc, 11, 0.5, 0.5, 0.5, "No runs recorded yet this season.", 12, y)
    end

    y = y - 14
    HSep(sc, 0.5, 0.4, 0.1, 0.6, 1, 12, y, 700)
    y = y - 10
    return y
end

-- ─── Affix Section ───────────────────────────────────────────────────────────



-- ─── Character Block ─────────────────────────────────────────────────────────

local function DrawCharBlock(sc, charKey, cdb, season, startY, showSummary, showTotal)
    local y   = startY
    local sdb = GetSeasonDB(charKey, season)

    -- Character name colored by class
    local classColor = RAID_CLASS_COLORS and cdb.class and RAID_CLASS_COLORS[cdb.class]
    local nr = classColor and classColor.r or 1
    local ng = classColor and classColor.g or 0.82
    local nb = classColor and classColor.b or 0

    local nameFS = PT(sc, 14, nr, ng, nb, cdb.name or charKey, 12, y)

    -- Rich tooltip hover on character name
    local nameHover = CreateFrame("Frame", nil, sc)
    nameHover:SetSize(160, 18)
    nameHover:SetPoint("TOPLEFT", sc, "TOPLEFT", 12, y + 2)
    nameHover:EnableMouse(true)
    do
        local hName      = cdb.name or charKey
        local hClass     = cdb.className or "Unknown"
        local hSpec      = cdb.spec or ""
        local hLevel     = cdb.level or 0
        local hIlvl      = cdb.ilvl or 0
        local hClassFile = cdb.class
        local hSeason    = season
        local hKey       = charKey
        nameHover:SetScript("OnEnter", function(self)
            local cc = RAID_CLASS_COLORS and hClassFile and RAID_CLASS_COLORS[hClassFile]
            local cr = cc and cc.r or 1
            local cg = cc and cc.g or 0.82
            local cb2 = cc and cc.b or 0
            local hex = cc and string.format("|cff%02x%02x%02x", cr*255, cg*255, cb2*255) or "|cffffff00"

            GameTooltip:SetOwner(self, "ANCHOR_CURSOR")
            GameTooltip:SetText(hex .. hName .. "|r")
            -- Level, spec, class, ilvl all on one subtitle line
            local subtitle = string.format("Level %d  %s %s",
                hLevel, hSpec ~= "" and hSpec or "", hClass)
            if hIlvl > 0 then
                subtitle = subtitle .. string.format("  |cffaaaaaa%d ilvl|r", hIlvl)
            end
            GameTooltip:AddLine(subtitle, 0.7, 0.7, 0.7)
            GameTooltip:AddLine(" ")

            -- Current keystone — only valid for the currently logged-in character
            -- GetOwnedKeystoneLevel() has no character parameter, always returns YOUR key
            if hKey == GetCharKey() then
                local keyLine = "|cff888888None|r"
                pcall(function()
                    local lv = C_MythicPlus and C_MythicPlus.GetOwnedKeystoneLevel
                               and C_MythicPlus.GetOwnedKeystoneLevel()
                    -- Fallback: active keystone info (works at instance entrance)
                    if not lv or lv == 0 then
                        if C_ChallengeMode and C_ChallengeMode.GetActiveKeystoneInfo then
                            lv = C_ChallengeMode.GetActiveKeystoneInfo()
                        end
                    end
                    if lv and lv > 0 then
                        -- Parse dungeon name directly from the link text [Keystone: DungeonName]
                        local mn
                        for bag = 0, 4 do
                            for slot = 1, (C_Container and C_Container.GetContainerNumSlots and C_Container.GetContainerNumSlots(bag)) or (GetContainerNumSlots and GetContainerNumSlots(bag)) or 0 do
                                local link = (C_Container and C_Container.GetContainerItemLink and C_Container.GetContainerItemLink(bag, slot)) or (GetContainerItemLink and GetContainerItemLink(bag, slot))
                                if link and link:lower():find("keystone") then
                                    -- Link format: ...|h[Keystone: Skyreach]|h|r
                                    local bracket = link:match("%[(.-)%]")
                                    if bracket then
                                        -- Strip "Keystone: " prefix
                                        local dungeonPart = bracket:gsub("^[Kk]eystone:%s*", "")
                                        if dungeonPart and dungeonPart ~= "" then
                                            mn = dungeonPart
                                        end
                                    end
                                end
                                if mn then break end
                            end
                            if mn then break end
                        end
                        keyLine = string.format("|cffffff00%s  +%d|r", mn or "Unknown", lv)
                    end
                end)
                GameTooltip:AddDoubleLine("Current Key", keyLine, 0.8,0.8,0.8, 1,1,1)
            end

            -- Season totals
            local sdb2  = GetSeasonDB(hKey, hSeason)
            local totT, totD, bestK = 0, 0, 0
            for _, dname in ipairs(DUNGEON_NAMES) do
                local e = sdb2[dname] or {}
                totT  = totT  + (e.timed    or 0)
                totD  = totD  + (e.depleted or 0)
                if (e.bestKey or 0) > bestK then bestK = e.bestKey end
            end
            local totAll  = totT + totD
            local totRate = totAll > 0 and math.floor((totT/totAll)*100+0.5) or 0
            local rr, rg, rb = totAll > 0 and GetTimingColor(totRate) or 0.5,0.5,0.5

            GameTooltip:AddLine(" ")
            GameTooltip:AddDoubleLine("Total Runs",   tostring(totAll),  0.8,0.8,0.8, 1,1,1)
            GameTooltip:AddDoubleLine("Timed",        tostring(totT),    0.8,0.8,0.8, 0.2,0.85,0.3)
            GameTooltip:AddDoubleLine("Depleted",     tostring(totD),    0.8,0.8,0.8, 0.9,0.25,0.25)
            if totAll > 0 then
                GameTooltip:AddDoubleLine("Timing Rate", string.format("%d%%", totRate), 0.8,0.8,0.8, rr,rg,rb)
            end
            if bestK > 0 then
                GameTooltip:AddDoubleLine("Best Key",  "+" .. bestK,     0.8,0.8,0.8, 1,0.82,0)
            end
            GameTooltip:Show()
        end)
        nameHover:SetScript("OnLeave", function() GameTooltip:Hide() end)
    end
    table.insert(rowPool, nameHover)

    -- Level + Spec
    local specStr  = cdb.spec      or ""
    local classStr = cdb.className or ""
    local subtitleStr
    if specStr ~= "" and classStr ~= "" then
        subtitleStr = string.format("Level %d  |cffaaaaaa[%s %s]|r", cdb.level or 0, specStr, classStr)
    else
        subtitleStr = string.format("Level %d", cdb.level or 0)
    end
    PT(sc, 11, 0.6, 0.6, 0.6, subtitleStr, 200, y)
    y = y - 22

    -- Best/Worst summary bar
    if showSummary then
        y = DrawSummaryBar(sc, charKey, season, y)
    end

    -- Column headers
    PT(sc, 11, 0.6,  0.6,  0.6,  "Dungeon",   12,  y)
    PT(sc, 11, 0.2,  0.85, 0.3,  "Completed", 230, y)
    PT(sc, 11, 0.9,  0.25, 0.25, "Depleted",  305, y)
    PT(sc, 11, 0.6,  0.8,  1,    "Timed %",   375, y)
    PT(sc, 11, 1,    0.82, 0,    "Best Key",  440, y)
    PT(sc, 11, 0.6,  0.8,  1,    "Last Run",  520, y)
    y = y - 4
    HSep(sc, 0.4, 0.35, 0.1, 0.7, 1, 12, y, 700)
    y = y - 18

    local totT, totD = 0, 0
    local stats = GetDungeonStats(charKey, season)

    for _, s in ipairs(stats) do
        totT, totD = totT + s.timed, totD + s.depleted

        -- Color the dungeon name by timing rate
        local nr, ng, nb
        if s.total == 0 then
            nr, ng, nb = 0.6, 0.6, 0.6
        else
            nr, ng, nb = GetTimingColor(s.rate)
        end
        PT(sc, 12, nr, ng, nb, s.name, 12, y)
        PT(sc, 12, 0.2,  0.85, 0.3,  tostring(s.timed),    230, y)
        PT(sc, 12, 0.9,  0.25, 0.25, tostring(s.depleted),  305, y)

        -- Timed %
        if s.total > 0 then
            local pr, pg, pb = GetTimingColor(s.rate)
            PT(sc, 12, pr, pg, pb, string.format("%d%%", s.rate), 375, y)
        else
            PT(sc, 12, 0.4, 0.4, 0.4, "--", 375, y)
        end

        -- Best key
        if s.bestKey > 0 then
            PT(sc, 12, 1, 0.82, 0, "+" .. s.bestKey, 440, y)
        else
            PT(sc, 12, 0.4, 0.4, 0.4, "--", 440, y)
        end

        -- Last run
        local lastStr = s.lastRun and FormatTimeAgo(s.lastRun) or "Never"
        local lr, lg, lb = 0.6, 0.8, 1
        if s.lastRun and s.lastRunTimed == false then lr,lg,lb = 0.9,0.25,0.25 end
        PT(sc, 11, lr, lg, lb, lastStr, 520, y)

        -- Hover tooltip
        local hover = CreateFrame("Frame", nil, sc)
        hover:SetSize(230, 20)
        hover:SetPoint("TOPLEFT", sc, "TOPLEFT", 12, y + 2)
        hover:EnableMouse(true)
        local hs, ht, hd, hbk, hlr, hlrt = s.name, s.timed, s.depleted, s.bestKey, s.lastRun, s.lastRunTimed
        local haffixes = s.affixes
        hover:SetScript("OnEnter", function(self)
            local total = ht + hd
            GameTooltip:SetOwner(self, "ANCHOR_CURSOR")
            GameTooltip:SetText("|cffffff00" .. hs .. "|r")
            GameTooltip:AddLine(" ")
            if total == 0 then
                GameTooltip:AddLine("No runs recorded yet.", 0.7, 0.7, 0.7)
            else
                local rate = math.floor((ht/total)*100+0.5)
                local rr,rg,rb = GetTimingColor(rate)
                GameTooltip:AddDoubleLine("|cff33dd55Completed (Timed)|r",  string.format("|cff33dd55%d  (%d%%)|r", ht, rate))
                GameTooltip:AddDoubleLine("|cffff4444Depleted (Failed)|r",  string.format("|cffff4444%d  (%d%%)|r", hd, 100-rate))
                GameTooltip:AddDoubleLine("Total Runs", tostring(total), 0.8,0.8,0.8, 0.8,0.8,0.8)
                if hbk > 0 then
                    GameTooltip:AddDoubleLine("|cffffff00Best Timed Key|r", "|cffffff00+" .. hbk .. "|r")
                end
                GameTooltip:AddLine(" ")
                GameTooltip:AddLine(string.format("You time %d%% of your runs here.", rate), rr, rg, rb)
                if rate >= 80 then
                    GameTooltip:AddLine("Exceptional performance!", 0.2, 0.85, 0.3)
                elseif rate >= 50 then
                    GameTooltip:AddLine("Solid — room to push higher.", 1, 0.82, 0)
                else
                    GameTooltip:AddLine("This dungeon needs work.", 0.9, 0.25, 0.25)
                end
                if hlr then
                    GameTooltip:AddLine(" ")
                    local lrStr = FormatTimeAgo(hlr)
                    local lrc = hlrt and "|cff33dd55" or "|cffff4444"
                    GameTooltip:AddLine("Last run: " .. lrc .. lrStr .. "|r", 0.7, 0.7, 0.7)
                end
            end
            GameTooltip:Show()
        end)
        hover:SetScript("OnLeave", function() GameTooltip:Hide() end)
        table.insert(rowPool, hover)

        y = y - 20
    end

    -- Totals row — always shown
    if showTotal then
        y = y - 4
        HSep(sc, 0.3, 0.3, 0.3, 0.5, 1, 12, y, 700)
        y = y - 16
        PT(sc, 12, 1, 1, 1,          "TOTAL",          12,  y)
        PT(sc, 12, 0.2, 0.85, 0.3,  tostring(totT),   230, y)
        PT(sc, 12, 0.9, 0.25, 0.25, tostring(totD),   305, y)
        local totTotal = totT + totD
        if totTotal > 0 then
            local totRate = math.floor((totT/totTotal)*100+0.5)
            local tr, tg, tb = GetTimingColor(totRate)
            PT(sc, 12, tr, tg, tb, string.format("%d%%", totRate), 375, y)
        end
    end

    y = y - 30
    return y
end

-- ─── Refresh Panel ───────────────────────────────────────────────────────────

local function VaultHasUnclaimedRewards()
    return false  -- disabled: Midnight API unreliable, prevents false flashing
end

local function DrawVaultStatusRow(sc, charKey, cdb, y)
    local wr    = GetWeeklyRuns(charKey)
    local slots = GetVaultSlotsFromRuns(wr)
    local hasRewards = (charKey == GetCharKey()) and VaultHasUnclaimedRewards() or false

    PT(sc, 11, 0.6, 0.6, 0.6,
        string.format("  %d runs this week", wr), 12, y)

    -- Three atlas chest icons
    for i, t in ipairs({1, 4, 8}) do
        local iconFrame = CreateFrame("Frame", nil, sc)
        iconFrame:SetSize(16, 16)
        iconFrame:SetPoint("TOPLEFT", sc, "TOPLEFT", 155 + (i - 1) * 20, y + 1)
        local tex = iconFrame:CreateTexture(nil, "ARTWORK")
        tex:SetAllPoints(iconFrame)
        tex:SetAtlas(wr >= t and "gficon-chest-evergreen-greatvault-complete"
                              or "gficon-chest-evergreen-greatvault-incomplete")
        table.insert(rowPool, iconFrame)
    end

    -- Tooltip hover over the whole row
    local rowHover = CreateFrame("Frame", nil, sc)
    rowHover:SetSize(430, 14)
    rowHover:SetPoint("TOPLEFT", sc, "TOPLEFT", 12, y + 1)
    rowHover:EnableMouse(true)
    rowHover:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, "ANCHOR_CURSOR")
        GameTooltip:SetText("|cffffff00Great Vault — M+ Progress|r")
        GameTooltip:AddLine(" ")
        for i, req in ipairs({1, 4, 8}) do
            local done = wr >= req
            local col  = done and "|cff00ff00" or "|cffff4444"
            local stat = done and "Unlocked" or string.format("%d / %d runs", wr, req)
            GameTooltip:AddDoubleLine("Slot " .. i, col .. stat .. "|r", 0.8,0.8,0.8, 1,1,1)
        end
        GameTooltip:AddLine(" ")
        if slots >= 3 then
            GameTooltip:AddLine("|cff00ff00All 3 slots unlocked!|r")
        elseif slots > 0 then
            GameTooltip:AddLine(string.format("|cffffff00%d slot%s ready.|r  Keep pushing for more!",
                slots, slots == 1 and "" or "s"))
        else
            GameTooltip:AddLine("|cff888888No slots unlocked yet.|r")
        end
        if hasRewards then
            GameTooltip:AddLine("|cffffff00Vault rewards are waiting — visit the Great Vault!|r")
        end
        GameTooltip:Show()
    end)
    rowHover:SetScript("OnLeave", function() GameTooltip:Hide() end)
    table.insert(rowPool, rowHover)

    -- Flash label only when HasAvailableRewards is true
    if hasRewards then
        local ff  = CreateFrame("Frame", nil, sc)
        ff:SetSize(130, 14)
        ff:SetPoint("TOPLEFT", sc, "TOPLEFT", 450, y + 1)
        local ffs = ff:CreateFontString(nil, "OVERLAY")
        ffs:SetFont("Fonts\\FRIZQT__.TTF", 11, "OUTLINE")
        ffs:SetTextColor(1, 0.82, 0, 1)
        ffs:SetText("[!] Vault Unopened")
        ffs:SetAllPoints(ff)
        ffs:SetJustifyH("LEFT")
        local ag   = ff:CreateAnimationGroup()
        ag:SetLooping("BOUNCE")
        local anim = ag:CreateAnimation("Alpha")
        anim:SetFromAlpha(0.2); anim:SetToAlpha(1.0)
        anim:SetDuration(0.6); anim:SetSmoothing("IN_OUT")
        ag:Play()
        table.insert(rowPool, ff)
    end

    return y - 18
end

local function RefreshPanel()
    if not panel then return end
    ClearRows()

    local season  = viewSeason or GetCurrentSeason()
    local myKey   = GetCharKey()
    local sc      = panel.scrollChild

    -- Update toggle buttons
    panel.btnIndividual:SetNormalFontObject(
        viewMode == "individual" and GameFontHighlight or GameFontNormal)
    panel.btnGlobal:SetNormalFontObject(
        viewMode == "global" and GameFontHighlight or GameFontNormal)

    local y = -8

    -- ── Season not started notice ─────────────────────────────────────────
    if not IsSeasonActive() then
        local secsLeft = SEASON_START_TIMESTAMP - time()
        local daysLeft = math.ceil(secsLeft / 86400)
        PT(sc, 12, 1, 0.82, 0,
            string.format("|cffffff00Season 1 hasn't started yet.|r  Opens in %d day%s  (March 24, 2026)",
                daysLeft, daysLeft == 1 and "" or "s"),
            12, y)
        y = y - 20
        HSep(sc, 0.3, 0.3, 0.3, 0.5, 1, 12, y, 700)
        y = y - 10
    end

    -- ── Individual-only header info ──────────────────────────────────────
    if viewMode == "individual" then

    -- ── Current keystone ─────────────────────────────────────────────────
    local keystoneText = nil
    pcall(function()
        local level, mapName

        -- Level via API
        if C_MythicPlus and C_MythicPlus.GetOwnedKeystoneLevel then
            level = C_MythicPlus.GetOwnedKeystoneLevel()
        end
        -- Fallback: active keystone (at instance entrance)
        if not level or level == 0 then
            if C_ChallengeMode and C_ChallengeMode.GetActiveKeystoneInfo then
                local lv, mid = C_ChallengeMode.GetActiveKeystoneInfo()
                if lv and lv > 0 then
                    level = lv
                    if mid and C_ChallengeMode.GetMapUIInfo then
                        mapName = C_ChallengeMode.GetMapUIInfo(mid)
                    end
                end
            end
        end

        -- Dungeon name: parse directly from the link text [Keystone: DungeonName]
        if level and level > 0 and not mapName then
            for bag = 0, 4 do
                for slot = 1, (C_Container and C_Container.GetContainerNumSlots and C_Container.GetContainerNumSlots(bag)) or (GetContainerNumSlots and GetContainerNumSlots(bag)) or 0 do
                    local link = (C_Container and C_Container.GetContainerItemLink and C_Container.GetContainerItemLink(bag, slot)) or (GetContainerItemLink and GetContainerItemLink(bag, slot))
                    if link and link:lower():find("keystone") then
                        local bracket = link:match("%[(.-)%]")
                        if bracket then
                            local dungeonPart = bracket:gsub("^[Kk]eystone:%s*", "")
                            if dungeonPart and dungeonPart ~= "" then
                                mapName = dungeonPart
                            end
                        end
                    end
                    if mapName then break end
                end
                if mapName then break end
            end
        end

        if level and level > 0 then
            if mapName and mapName ~= "" then
                -- Shorten to our list name if it's a long API name
                for _, dname in ipairs(DUNGEON_NAMES) do
                    if mapName:find(dname, 1, true) then mapName = dname; break end
                end
            end
            keystoneText = string.format("|cffffff00Current Key:|r  %s  |cffffff00+%d|r",
                (mapName and mapName ~= "") and mapName or "Unknown", level)
        end
    end)

    if keystoneText then
        PT(sc, 11, 1, 1, 1, keystoneText, 12, y)
    else
        PT(sc, 11, 0.55, 0.55, 0.55, "|cffffff00Current Key:|r  |cff888888None|r", 12, y)
    end
    y = y - 18

    -- ── Current week's affixes ────────────────────────────────────────────
    local affixList = nil
    local mockAff = MythicTrackerSeasonDB and MythicTrackerSeasonDB._mockAffixes
    if mockAff then
        affixList = {}
        for _, n in ipairs(mockAff) do table.insert(affixList, { name = n }) end
    else
        pcall(function()
            if C_MythicPlus and C_MythicPlus.GetCurrentAffixes then
                affixList = C_MythicPlus.GetCurrentAffixes()
            end
        end)
    end

    local affixNames = {}
    if affixList then
        for _, a in ipairs(affixList) do
            local name = a.name
            if not name and a.id then
                pcall(function()
                    if C_ChallengeMode and C_ChallengeMode.GetAffixInfo then
                        name = select(1, C_ChallengeMode.GetAffixInfo(a.id))
                    end
                end)
            end
            if name then table.insert(affixNames, name) end
        end
    end

    local affixLineText
    if #affixNames > 0 then
        affixLineText = "|cffffff00This Week:|r  " .. table.concat(affixNames, "  |cff555555•|r  ")
    else
        affixLineText = "|cffffff00This Week:|r  |cff888888Unknown|r"
    end
    PT(sc, 11, 1, 1, 1, affixLineText, 12, y)

    y = y - 18
    HSep(sc, 0.3, 0.3, 0.3, 0.5, 1, 12, y, 700)
    y = y - 10

    -- ── Weekly run count + vault progress ────────────────────────────────
    do
        local weekRuns   = GetWeeklyRuns(myKey)
        local slots      = GetVaultSlotsFromRuns(weekRuns)
        local hasRewards = VaultHasUnclaimedRewards()

        local runsColor = weekRuns == 0 and "|cff888888" or "|cffffffff"
        local runsText  = string.format("|cffffff00Keys This Week:|r  %s%d|r", runsColor, weekRuns)
        PT(sc, 11, 1, 1, 1, runsText, 12, y)

        -- Vault label
        PT(sc, 11, 1, 0.82, 0, "Vault:", 260, y)

        -- Three atlas chest icons for the 3 vault slots
        local thresholds = {1, 4, 8}
        for i, t in ipairs(thresholds) do
            local iconFrame = CreateFrame("Frame", nil, sc)
            iconFrame:SetSize(18, 18)
            iconFrame:SetPoint("TOPLEFT", sc, "TOPLEFT", 300 + (i - 1) * 24, y + 2)
            local tex = iconFrame:CreateTexture(nil, "ARTWORK")
            tex:SetAllPoints(iconFrame)
            local atlas
            if weekRuns >= t then
                atlas = "gficon-chest-evergreen-greatvault-complete"
            else
                atlas = "gficon-chest-evergreen-greatvault-incomplete"
            end
            tex:SetAtlas(atlas)
            table.insert(rowPool, iconFrame)
        end

        -- Threshold hint
        PT(sc, 10, 0.45, 0.45, 0.45, "(1 / 4 / 8 runs)", 378, y)

        -- Flash only when HasAvailableRewards() confirms rewards are waiting
        if hasRewards then
            local flashFrame = CreateFrame("Frame", nil, sc)
            flashFrame:SetSize(120, 14)
            flashFrame:SetPoint("TOPLEFT", sc, "TOPLEFT", 520, y + 1)
            local flashFS = flashFrame:CreateFontString(nil, "OVERLAY")
            flashFS:SetFont("Fonts\\FRIZQT__.TTF", 11, "OUTLINE")
            flashFS:SetTextColor(1, 0.82, 0, 1)
            flashFS:SetText("[!] Vault Unopened")
            flashFS:SetAllPoints(flashFrame)
            flashFS:SetJustifyH("LEFT")
            local ag = flashFrame:CreateAnimationGroup()
            ag:SetLooping("BOUNCE")
            local anim = ag:CreateAnimation("Alpha")
            anim:SetFromAlpha(0.2); anim:SetToAlpha(1.0)
            anim:SetDuration(0.6); anim:SetSmoothing("IN_OUT")
            ag:Play()
            table.insert(rowPool, flashFrame)
        end

        local vaultHover = CreateFrame("Frame", nil, sc)
        vaultHover:SetSize(340, 14)
        vaultHover:SetPoint("TOPLEFT", sc, "TOPLEFT", 260, y + 1)
        vaultHover:EnableMouse(true)
        vaultHover:SetScript("OnEnter", function(self)
            GameTooltip:SetOwner(self, "ANCHOR_CURSOR")
            GameTooltip:SetText("|cffffff00Great Vault — M+ Progress|r")
            GameTooltip:AddLine(" ")
            for i, req in ipairs({1, 4, 8}) do
                local done = weekRuns >= req
                local col  = done and "|cff00ff00" or "|cffff4444"
                local stat = done and "Unlocked" or string.format("%d / %d runs", weekRuns, req)
                GameTooltip:AddDoubleLine("Slot " .. i, col .. stat .. "|r", 0.8,0.8,0.8, 1,1,1)
            end
            GameTooltip:AddLine(" ")
            if slots >= 3 then
                GameTooltip:AddLine("|cff00ff00All 3 slots unlocked! Visit the Great Vault.|r")
            elseif slots > 0 then
                GameTooltip:AddLine(string.format("|cffffff00%d slot%s ready.|r  Keep pushing for more!",
                    slots, slots == 1 and "" or "s"))
            else
                GameTooltip:AddLine("|cff888888Complete 1 key to unlock your first slot.|r")
            end
            if hasRewards then
                GameTooltip:AddLine("|cffffff00Vault rewards are waiting — visit the Great Vault!|r")
            end
            GameTooltip:Show()
        end)
        vaultHover:SetScript("OnLeave", function() GameTooltip:Hide() end)
        table.insert(rowPool, vaultHover)

        y = y - 18
        HSep(sc, 0.3, 0.3, 0.3, 0.5, 1, 12, y, 700)
        y = y - 10
    end

    end -- viewMode == "individual"

    if viewMode == "individual" then
        local cdb = GetCharDB(myKey)
        y = DrawCharBlock(sc, myKey, cdb, season, y, true, true)
    else
        -- Build sorted alt list (current char first, then others alphabetically)
        local altList = {}
        local cdb0 = GetCharDB(myKey)
        if (cdb0.level or 0) >= MIN_LEVEL then
            table.insert(altList, { key=myKey, cdb=cdb0 })
        end
        for key, cdb2 in pairs(MythicTrackerSeasonDB) do
            if type(cdb2) == "table" and cdb2.name and key ~= myKey
               and (cdb2.level or 0) >= MIN_LEVEL then
                table.insert(altList, { key=key, cdb=cdb2 })
            end
        end
        table.sort(altList, function(a, b)
            if a.key == myKey then return true end
            if b.key == myKey then return false end
            return (a.cdb.name or a.key) < (b.cdb.name or b.key)
        end)

        -- Default selection
        if not selectedAltKey then
            selectedAltKey = altList[1] and altList[1].key or myKey
        end
        -- Ensure selection is still valid
        local selValid = false
        for _, e in ipairs(altList) do
            if e.key == selectedAltKey then selValid = true; break end
        end
        if not selValid then selectedAltKey = altList[1] and altList[1].key or myKey end

        -- ── Dropdown button ──────────────────────────────────────────────
        local dropBtn = CreateFrame("Button", nil, sc, "UIPanelButtonTemplate")
        dropBtn:SetSize(200, 24)
        dropBtn:SetPoint("TOPLEFT", sc, "TOPLEFT", 12, y)

        -- Find current selection display name
        local selName = selectedAltKey
        for _, e in ipairs(altList) do
            if e.key == selectedAltKey then
                selName = (e.cdb.name or selectedAltKey)
                if e.key == myKey then selName = selName .. " (You)" end
                break
            end
        end
        dropBtn:SetText(selName .. "  v")

        dropBtn:SetScript("OnClick", function(self)
            -- Build a simple dropdown menu using UIDropDownMenu or a custom frame
            local menu = CreateFrame("Frame", "MTSAltDropdown", UIParent, "BackdropTemplate")
            menu:SetFrameStrata("TOOLTIP")
            menu:SetBackdrop({
                bgFile   = "Interface\\DialogFrame\\UI-DialogBox-Background-Dark",
                edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
                tile=true, tileSize=16, edgeSize=12,
                insets = { left=3, right=3, top=3, bottom=3 },
            })
            menu:SetBackdropColor(0.05, 0.04, 0.08, 0.98)

            local btnH   = 22
            menu:SetWidth(200)
            menu:SetHeight(btnH * #altList + 6)
            menu:SetPoint("TOPLEFT", self, "BOTTOMLEFT", 0, 0)
            menu:EnableMouse(true)

            for i, e in ipairs(altList) do
                local entry = CreateFrame("Button", nil, menu)
                entry:SetSize(194, btnH)
                entry:SetPoint("TOPLEFT", menu, "TOPLEFT", 3, -3 - (i-1)*btnH)
                entry:EnableMouse(true)

                local hl = entry:CreateTexture(nil, "HIGHLIGHT")
                hl:SetColorTexture(1, 0.82, 0, 0.15)
                hl:SetAllPoints(entry)

                local fs = entry:CreateFontString(nil, "OVERLAY")
                fs:SetFont("Fonts\\FRIZQT__.TTF", 12, "OUTLINE")
                fs:SetJustifyH("LEFT")
                fs:SetPoint("LEFT", entry, "LEFT", 6, 0)

                local cc = RAID_CLASS_COLORS and e.cdb.class and RAID_CLASS_COLORS[e.cdb.class]
                local r = cc and cc.r or 1
                local g = cc and cc.g or 0.82
                local b = cc and cc.b or 0
                fs:SetTextColor(r, g, b, 1)

                local label = e.cdb.name or e.key
                if e.key == myKey then label = label .. " (You)" end
                fs:SetText(label)

                local capKey = e.key
                entry:SetScript("OnClick", function()
                    selectedAltKey = capKey
                    menu:Hide()
                    RefreshPanel()
                end)
            end

            -- Close when clicking anywhere outside by watching cursor leaving the menu
            menu:SetScript("OnLeave", function(self)
                C_Timer.After(0.15, function()
                    if menu:IsShown() and not menu:IsMouseOver() then
                        menu:Hide()
                    end
                end)
            end)

            table.insert(rowPool, menu)
        end)
        table.insert(rowPool, dropBtn)

        y = y - 32

        -- ── Selected alt display ─────────────────────────────────────────
        for _, e in ipairs(altList) do
            if e.key == selectedAltKey then
                y = DrawCharBlock(sc, e.key, e.cdb, season, y, true, true)
                y = DrawVaultStatusRow(sc, e.key, e.cdb, y)
                break
            end
        end
    end

    sc:SetHeight(math.abs(y) + 30)
end

-- ─── Static Popup for Reset ──────────────────────────────────────────────────

StaticPopupDialogs["MTSRESET_CONFIRM"] = {
    text         = "Reset all M+ data for |cffffffff%s|r? This cannot be undone.",
    button1      = "Reset",
    button2      = "Cancel",
    OnAccept     = function(self, charKey)
        if charKey and MythicTrackerSeasonDB[charKey] then
            local name = MythicTrackerSeasonDB[charKey].name
            local level = MythicTrackerSeasonDB[charKey].level
            MythicTrackerSeasonDB[charKey] = { name=name, level=level }
            print("|cffff9900[MythicTracker]|r Data reset for " .. (name or charKey))
            RefreshPanel()
        end
    end,
    timeout      = 0,
    whileDead    = true,
    hideOnEscape = true,
    preferredIndex = 3,
}

local copyPopup = nil
local function BuildCopyPopup()
    if copyPopup then return end

    copyPopup = CreateFrame("Frame", "MythicTrackerCopyFrame", UIParent, "BackdropTemplate")
    copyPopup:SetSize(520, 200)
    copyPopup:SetPoint("CENTER", UIParent, "CENTER", 0, 60)
    copyPopup:SetFrameStrata("TOOLTIP")
    copyPopup:SetMovable(true)
    copyPopup:EnableMouse(true)
    copyPopup:RegisterForDrag("LeftButton")
    copyPopup:SetScript("OnDragStart", copyPopup.StartMoving)
    copyPopup:SetScript("OnDragStop",  copyPopup.StopMovingOrSizing)
    copyPopup:Hide()

    copyPopup:SetBackdrop({
        bgFile   = "Interface\\DialogFrame\\UI-DialogBox-Background-Dark",
        edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
        tile=true, tileSize=32, edgeSize=28,
        insets = { left=8, right=9, top=9, bottom=8 },
    })
    copyPopup:SetBackdropColor(0.05, 0.04, 0.08, 0.98)

    local title = copyPopup:CreateFontString(nil, "OVERLAY")
    title:SetFont("Fonts\\FRIZQT__.TTF", 13, "OUTLINE")
    title:SetTextColor(1, 0.82, 0, 1)
    title:SetText("Copy Export — Press Ctrl+A then Ctrl+C")
    title:SetPoint("TOP", copyPopup, "TOP", 0, -14)

    local sub = copyPopup:CreateFontString(nil, "OVERLAY")
    sub:SetFont("Fonts\\FRIZQT__.TTF", 10, "OUTLINE")
    sub:SetTextColor(0.5, 0.5, 0.5, 1)
    sub:SetText("Paste into Discord or anywhere else")
    sub:SetPoint("TOP", title, "BOTTOM", 0, -2)

    local eb = CreateFrame("EditBox", nil, copyPopup)
    eb:SetMultiLine(true)
    eb:SetFontObject(ChatFontNormal)
    eb:SetWidth(480)
    eb:SetHeight(120)
    eb:SetPoint("TOP", sub, "BOTTOM", 0, -8)
    eb:SetAutoFocus(true)
    eb:SetScript("OnEscapePressed", function() copyPopup:Hide() end)
    copyPopup.editbox = eb

    local closeBtn = CreateFrame("Button", nil, copyPopup, "UIPanelCloseButton")
    closeBtn:SetPoint("TOPRIGHT", copyPopup, "TOPRIGHT", 2, 2)
    closeBtn:SetScript("OnClick", function() copyPopup:Hide() end)
end

local function ShowCopyPopup(text)
    BuildCopyPopup()
    copyPopup.editbox:SetText(text)
    copyPopup.editbox:HighlightText()
    copyPopup:Show()
    copyPopup.editbox:SetFocus()
end

-- ─── Export to Chat ──────────────────────────────────────────────────────────

local function ExportToChat()
    local season  = viewSeason or GetCurrentSeason()
    local myKey   = GetCharKey()
    local cdb     = GetCharDB(myKey)
    local sdb     = GetSeasonDB(myKey, season)

    local lines = {}
    table.insert(lines, string.format("[MythicTracker] %s — Season %d M+ Summary:",
        cdb.name or myKey, season))

    local totT, totD = 0, 0
    for _, name in ipairs(DUNGEON_NAMES) do
        local e = sdb[name] or {}
        local t = e.timed    or 0
        local d = e.depleted or 0
        totT, totD = totT + t, totD + d
        local total = t + d
        local rate  = total > 0 and math.floor((t/total)*100+0.5) or 0
        local bk    = (e.bestKey or 0) > 0 and ("+" .. e.bestKey) or "--"
        table.insert(lines, string.format("  %-24s  %d timed  %d depleted  Best: %s  (%d%%)",
            name, t, d, bk, rate))
    end
    table.insert(lines, string.format("  %-24s  %d timed  %d depleted", "TOTAL:", totT, totD))

    local exportText = table.concat(lines, "\n")
    ShowCopyPopup(exportText)
end

-- ─── Build Panel ─────────────────────────────────────────────────────────────

local function ClosePanel()
    if panel then panel:Hide() end
    if seasonBtn then seasonBtn:SetButtonState("NORMAL") end
end

local function RestorePosition()
    panel:ClearAllPoints()
    local x = MythicTrackerSeasonDB and MythicTrackerSeasonDB.panelX
    local y = MythicTrackerSeasonDB and MythicTrackerSeasonDB.panelY
    if x and y then
        panel:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", x, y)
    else
        panel:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
    end
end

-- ─── LFG Content ─────────────────────────────────────────────────────────────

local function DrawLFGContent(sc)
    local y = -8
    local myKey = GetCharKey()
    local db    = GetLFGDB(myKey)
    local cdb   = GetCharDB(myKey)

    -- Character header
    local classColor = RAID_CLASS_COLORS and cdb.class and RAID_CLASS_COLORS[cdb.class]
    local nr = classColor and classColor.r or 1
    local ng = classColor and classColor.g or 0.82
    local nb = classColor and classColor.b or 0
    PT(sc, 14, nr, ng, nb, cdb.name or myKey, 12, y)
    local specStr  = cdb.spec      or ""
    local classStr = cdb.className or ""
    if specStr ~= "" and classStr ~= "" then
        PT(sc, 11, 0.6, 0.6, 0.6,
            string.format("Level %d  |cffaaaaaa[%s %s]|r", cdb.level or 0, specStr, classStr),
            200, y)
    end
    y = y - 26

    -- Summary boxes
    local total    = (db.invited or 0) + (db.declined or 0)
    local decPct   = total > 0 and math.floor(((db.declined or 0)/total)*100+0.5) or 0
    local invPct   = total > 0 and (100 - decPct) or 0

    HSep(sc, 0.4, 0.35, 0.1, 0.6, 1, 12, y, 700)
    y = y - 18

    -- Big summary numbers
    PT(sc, 11, 0.6, 0.6, 0.6, "Total Applications", 12,  y)
    PT(sc, 11, 0.6, 0.6, 0.6, "Invited",            200, y)
    PT(sc, 11, 0.6, 0.6, 0.6, "Declined",           340, y)
    PT(sc, 11, 0.6, 0.6, 0.6, "Accept Rate",        480, y)
    y = y - 18

    PT(sc, 20, 1,    1,    1,    tostring(total),            12,  y)
    PT(sc, 20, 0.2,  0.85, 0.3,  tostring(db.invited or 0),  200, y)
    PT(sc, 20, 0.9,  0.25, 0.25, tostring(db.declined or 0), 340, y)
    local ar, ag, ab = GetTimingColor(invPct)
    PT(sc, 20, ar, ag, ab, string.format("%d%%", invPct), 480, y)
    y = y - 32

    HSep(sc, 0.4, 0.35, 0.1, 0.6, 1, 12, y, 700)
    y = y - 16

    -- Per-dungeon breakdown (only if data exists)
    local hasDungeons = false
    for _ in pairs(db.dungeons) do hasDungeons = true; break end

    if hasDungeons then
        PT(sc, 13, 1, 0.82, 0, "Per-Dungeon Breakdown", 12, y)
        y = y - 20

        PT(sc, 11, 0.6, 0.6, 0.6,  "Dungeon",      12,  y)
        PT(sc, 11, 0.2, 0.85, 0.3, "Invited",       260, y)
        PT(sc, 11, 0.9, 0.25, 0.25,"Declined",      360, y)
        PT(sc, 11, 0.6, 0.8, 1,    "Accept Rate",   460, y)
        y = y - 4
        HSep(sc, 0.4, 0.35, 0.1, 0.5, 1, 12, y, 700)
        y = y - 18

        for _, dname in ipairs(DUNGEON_NAMES) do
            local d = db.dungeons[dname]
            if d then
                local dt   = (d.invited or 0) + (d.declined or 0)
                local dPct = dt > 0 and math.floor(((d.invited or 0)/dt)*100+0.5) or 0
                local dr, dg, db2 = GetTimingColor(dPct)
                PT(sc, 12, 0.9, 0.85, 0.6, dname,                     12,  y)
                PT(sc, 12, 0.2, 0.85, 0.3, tostring(d.invited  or 0), 260, y)
                PT(sc, 12, 0.9, 0.25, 0.25,tostring(d.declined or 0), 360, y)
                PT(sc, 12, dr, dg, db2,    string.format("%d%%", dPct), 460, y)
                y = y - 20
            end
        end
    else
        PT(sc, 12, 0.5, 0.5, 0.5,
            "No dungeon-specific data yet. Dungeon names are matched from listing titles.", 12, y)
        y = y - 20
    end

    y = y - 10
    HSep(sc, 0.3, 0.3, 0.3, 0.4, 1, 12, y, 700)
    y = y - 14
    PT(sc, 10, 0.4, 0.4, 0.4,
        "Tracks LFG applications via the Premade Group Finder. Only explicit leader declines are counted.", 12, y)
    y = y - 20

    sc:SetHeight(math.abs(y) + 30)
end

local function RefreshLFG()
    if not panel then return end
    ClearRows()
    local sc = panel.scrollChild
    DrawLFGContent(sc)
end

local function RefreshHistory()
    if not panel then return end
    ClearRows()
    local sc = panel.scrollChild
    local y  = -8

    -- Collect all runs across all chars, newest first
    local allRuns = {}
    for key, cdb in pairs(MythicTrackerSeasonDB) do
        if type(cdb) == "table" and cdb.name and cdb.runHistory then
            for _, run in ipairs(cdb.runHistory) do
                table.insert(allRuns, {
                    charKey   = key,
                    charName  = cdb.name,
                    charClass = cdb.class,
                    run       = run,
                })
            end
        end
    end
    table.sort(allRuns, function(a, b)
        return (a.run.timestamp or 0) > (b.run.timestamp or 0)
    end)

    if #allRuns == 0 then
        PT(sc, 12, 0.5, 0.5, 0.5,
            "No runs recorded yet.  Complete a Mythic+ dungeon to start tracking.", 12, y)
        return
    end

    -- Column headers
    PT(sc, 11, 0.6, 0.6, 0.6, "Date / Time",  12,  y)
    PT(sc, 11, 0.6, 0.6, 0.6, "Character",    155, y)
    PT(sc, 11, 0.6, 0.6, 0.6, "Dungeon",      285, y)
    PT(sc, 11, 0.6, 0.6, 0.6, "Key",          460, y)
    PT(sc, 11, 0.6, 0.6, 0.6, "Result",       505, y)
    PT(sc, 11, 0.6, 0.6, 0.6, "Affixes",      570, y)
    y = y - 4
    HSep(sc, 0.4, 0.35, 0.1, 0.8, 1, 12, y, 730)
    y = y - 18

    for idx, entry in ipairs(allRuns) do
        local run  = entry.run
        local bg   = (idx % 2 == 0) and 0.04 or 0.0

        -- Alternating row tint
        local rowBg = sc:CreateTexture(nil, "BACKGROUND")
        rowBg:SetColorTexture(1, 1, 1, bg)
        rowBg:SetPoint("TOPLEFT",  sc, "TOPLEFT",   12, y + 2)
        rowBg:SetPoint("TOPRIGHT", sc, "TOPRIGHT", -13, y + 2)
        rowBg:SetHeight(18)

        -- Date/time
        PT(sc, 11, 0.55, 0.55, 0.55, FormatDateTime(run.timestamp), 12, y)

        -- Character name in class colour
        local cc    = RAID_CLASS_COLORS and entry.charClass and RAID_CLASS_COLORS[entry.charClass]
        local cr    = cc and cc.r or 1
        local cg    = cc and cc.g or 0.82
        local cb    = cc and cc.b or 0
        PT(sc, 12, cr, cg, cb, entry.charName or entry.charKey, 155, y)

        -- Dungeon
        PT(sc, 12, 0.9, 0.85, 0.6, run.dungeon or "Unknown", 285, y)

        -- Key level
        local lvl = run.level or 0
        PT(sc, 12, 1, 0.82, 0, lvl > 0 and ("+"..lvl) or "--", 460, y)

        -- Result
        if run.timed then
            PT(sc, 12, 0.2, 0.85, 0.3, "Timed",    505, y)
        else
            PT(sc, 12, 0.9, 0.25, 0.25, "Depleted", 505, y)
        end

        -- Affixes (abbreviated — first letter of each)
        local affixStr = ""
        if run.affixes and #run.affixes > 0 then
            local parts = {}
            for _, a in ipairs(run.affixes) do
                table.insert(parts, a:sub(1, 4))
            end
            affixStr = table.concat(parts, " • ")
        end
        PT(sc, 10, 0.5, 0.5, 0.7, affixStr, 570, y)

        -- Hover: full run details + group
        local hover = CreateFrame("Frame", nil, sc)
        hover:SetHeight(18)
        hover:SetPoint("TOPLEFT",  sc, "TOPLEFT",   12, y + 2)
        hover:SetPoint("TOPRIGHT", sc, "TOPRIGHT", -13, y + 2)
        hover:EnableMouse(true)
        local capturedEntry = entry
        hover:SetScript("OnEnter", function(self)
            local r = capturedEntry.run
            GameTooltip:SetOwner(self, "ANCHOR_CURSOR")

            -- Title line
            local resultCol = r.timed and "|cff33dd55" or "|cffdd3333"
            local resultStr = r.timed and "Timed" or "Depleted"
            GameTooltip:SetText(string.format("%s +%d  %s%s|r",
                r.dungeon or "Unknown", r.level or 0, resultCol, resultStr))

            -- Date
            GameTooltip:AddLine(FormatDateTime(r.timestamp), 0.6, 0.6, 0.6)
            GameTooltip:AddLine(" ")

            -- Affixes
            if r.affixes and #r.affixes > 0 then
                GameTooltip:AddLine("|cffffff00Affixes:|r  " .. table.concat(r.affixes, "  •  "))
                GameTooltip:AddLine(" ")
            end

            -- Group
            if r.group and #r.group > 0 then
                GameTooltip:AddLine("|cffffff00Group|r", 1, 1, 1)
                for _, m in ipairs(r.group) do
                    local mc  = RAID_CLASS_COLORS and m.class and RAID_CLASS_COLORS[m.class]
                    local mr  = mc and mc.r or 0.8
                    local mg  = mc and mc.g or 0.8
                    local mb2 = mc and mc.b or 0.8
                    local specLine = m.spec ~= "" and (m.spec .. " " .. m.className) or m.className
                    local scoreLine = m.score > 0 and string.format("|cffaaaaaa  [%d]|r", m.score) or ""
                    GameTooltip:AddLine(string.format("  %s  |cff888888%s|r%s",
                        m.name, specLine, scoreLine), mr, mg, mb2)
                end
            else
                GameTooltip:AddLine("|cff888888Group data unavailable|r")
            end
            GameTooltip:Show()
        end)
        hover:SetScript("OnLeave", function() GameTooltip:Hide() end)
        table.insert(rowPool, hover)

        y = y - 20
    end
end

local function BuildPanel()
    if panel then return end

    panel = CreateFrame("Frame", "MythicTrackerSeasonPanel", UIParent, "BackdropTemplate")
    panel:SetSize(760, 560)
    panel:SetFrameStrata("DIALOG")
    panel:SetFrameLevel(100)
    panel:SetMovable(true)
    panel:EnableMouse(true)
    panel:RegisterForDrag("LeftButton")
    panel:SetScript("OnDragStart", panel.StartMoving)
    panel:SetScript("OnDragStop", function(self)
        self:StopMovingOrSizing()
        MythicTrackerSeasonDB.panelX = self:GetLeft()
        MythicTrackerSeasonDB.panelY = self:GetTop()
    end)
    panel:Hide()
    -- Register with UISpecialFrames so Escape key closes it
    tinsert(UISpecialFrames, "MythicTrackerSeasonPanel")

    panel:SetBackdrop({
        bgFile   = "Interface\\DialogFrame\\UI-DialogBox-Background-Dark",
        edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
        tile=true, tileSize=32, edgeSize=32,
        insets = { left=11, right=12, top=12, bottom=11 },
    })
    panel:SetBackdropColor(0.05, 0.04, 0.08, 0.98)

    -- Title bar bg
    local titleBg = panel:CreateTexture(nil, "ARTWORK")
    titleBg:SetColorTexture(0.08, 0.06, 0.02, 1)
    titleBg:SetPoint("TOPLEFT",  panel, "TOPLEFT",   12, -12)
    titleBg:SetPoint("TOPRIGHT", panel, "TOPRIGHT",  -13, -12)
    titleBg:SetHeight(32)

    -- Title
    local titleFS = panel:CreateFontString(nil, "OVERLAY")
    titleFS:SetFont("Fonts\\FRIZQT__.TTF", 15, "OUTLINE")
    titleFS:SetTextColor(1, 0.82, 0, 1)
    titleFS:SetText("Seasonal M+ Runs")
    titleFS:SetPoint("LEFT", titleBg, "LEFT", 10, 0)

    -- Gold separator under title
    local sep = panel:CreateTexture(nil, "ARTWORK")
    sep:SetColorTexture(0.6, 0.5, 0.1, 0.8)
    sep:SetPoint("TOPLEFT",  panel, "TOPLEFT",  12, -44)
    sep:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -13, -44)
    sep:SetHeight(1)

    -- Close button
    local closeBtn = CreateFrame("Button", nil, panel, "UIPanelCloseButton")
    closeBtn:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -2, -4)
    closeBtn:SetScript("OnClick", ClosePanel)

    -- ── Tab buttons ──────────────────────────────────────────────────────
    local tabMplus = CreateFrame("Button", nil, panel, "PanelTabButtonTemplate")
    tabMplus:SetText("M+ Runs")
    tabMplus:SetPoint("TOPLEFT", panel, "BOTTOMLEFT", 14, 20)
    PanelTemplates_TabResize(tabMplus, 0)
    for _, k in ipairs({"LeftActive","MiddleActive","RightActive"}) do
        if tabMplus[k] then tabMplus[k]:Hide(); tabMplus[k].Show = function() end end
    end

    local tabLFG = CreateFrame("Button", nil, panel, "PanelTabButtonTemplate")
    tabLFG:SetText("LFG Activity")
    tabLFG:SetPoint("LEFT", tabMplus, "RIGHT", 4, 0)
    PanelTemplates_TabResize(tabLFG, 0)
    for _, k in ipairs({"LeftActive","MiddleActive","RightActive"}) do
        if tabLFG[k] then tabLFG[k]:Hide(); tabLFG[k].Show = function() end end
    end

    local tabHistory = CreateFrame("Button", nil, panel, "PanelTabButtonTemplate")
    tabHistory:SetText("M+ Run History")
    tabHistory:SetPoint("LEFT", tabLFG, "RIGHT", 4, 0)
    PanelTemplates_TabResize(tabHistory, 0)
    for _, k in ipairs({"LeftActive","MiddleActive","RightActive"}) do
        if tabHistory[k] then tabHistory[k]:Hide(); tabHistory[k].Show = function() end end
    end

    -- Great Vault anchored to far right
    local tabVault = CreateFrame("Button", nil, panel, "PanelTabButtonTemplate")
    tabVault:SetText("Great Vault")
    tabVault:SetPoint("BOTTOMRIGHT", panel, "BOTTOMRIGHT", -14, -20)
    PanelTemplates_TabResize(tabVault, 0)
    for _, k in ipairs({"LeftActive","MiddleActive","RightActive"}) do
        if tabVault[k] then tabVault[k]:Hide(); tabVault[k].Show = function() end end
    end

    -- Glow: pulsing gold border when vault threshold hit
    local glowTex = tabVault:CreateTexture(nil, "OVERLAY")
    glowTex:SetAllPoints(tabVault)
    glowTex:SetColorTexture(1, 0.82, 0.1, 1)
    glowTex:SetBlendMode("ADD")
    glowTex:SetAlpha(0)

    local vaultGlowAG = glowTex:CreateAnimationGroup()
    vaultGlowAG:SetLooping("BOUNCE")
    local glowAnim = vaultGlowAG:CreateAnimation("Alpha")
    glowAnim:SetFromAlpha(0.0)
    glowAnim:SetToAlpha(0.35)
    glowAnim:SetDuration(0.85)
    glowAnim:SetSmoothing("IN_OUT")
    panel.vaultGlowAG = vaultGlowAG

    tabVault:SetScript("OnClick", function()
        ClosePanel()
        C_Timer.After(0.05, function()
            if LoadAddOn then pcall(LoadAddOn, "Blizzard_WeeklyRewards") end
            if WeeklyRewards_ShowUI then
                pcall(WeeklyRewards_ShowUI)
            elseif WeeklyRewardsFrame then
                pcall(ShowUIPanel, WeeklyRewardsFrame)
            else
                print("|cffff9900[MythicTracker]|r Great Vault isn't available right now.")
            end
        end)
    end)

    tabVault:SetScript("OnEnter", function(self)
        local slots, prog = GetVaultMythicProgress()
        GameTooltip:SetOwner(self, "ANCHOR_TOP")
        GameTooltip:SetText("|cffffff00Great Vault|r")
        GameTooltip:AddLine(" ")
        if slots >= 3 then
            GameTooltip:AddLine("|cff00ff00All 3 reward slots unlocked!|r")
        elseif slots == 2 then
            GameTooltip:AddLine("|cff00ff002 / 3 slots unlocked|r")
            GameTooltip:AddDoubleLine("Next slot:", string.format("%d / 8 runs", prog), 0.8,0.8,0.8, 1,0.82,0)
        elseif slots == 1 then
            GameTooltip:AddLine("|cffffff001 / 3 slots unlocked|r")
            GameTooltip:AddDoubleLine("Next slot:", string.format("%d / 4 runs", prog), 0.8,0.8,0.8, 1,0.82,0)
        else
            GameTooltip:AddLine("|cff888888No slots unlocked yet|r")
            GameTooltip:AddDoubleLine("First slot:", string.format("%d / 1 run", prog), 0.8,0.8,0.8, 1,0.82,0)
        end
        GameTooltip:AddLine(" ")
        GameTooltip:AddLine("Click to open the Great Vault", 0.5, 0.5, 0.5)
        GameTooltip:Show()
    end)
    tabVault:SetScript("OnLeave", function() GameTooltip:Hide() end)

    -- ── M+ control row ───────────────────────────────────────────────────
    local mplusControls = {}
    local function MakeBtn(label, x, w)
        local b = CreateFrame("Button", nil, panel, "UIPanelButtonTemplate")
        b:SetSize(w or 120, 22)
        b:SetText(label)
        b:SetPoint("TOPLEFT", panel, "TOPLEFT", x, -50)
        table.insert(mplusControls, b)
        return b
    end

    panel.btnIndividual = MakeBtn("This Character",  14,  120)
    panel.btnGlobal     = MakeBtn("All Characters",  140, 120)
    panel.btnIndividual:SetScript("OnClick", function() viewMode = "individual"; RefreshPanel() end)
    panel.btnGlobal:SetScript("OnClick",     function() viewMode = "global";     RefreshPanel() end)

    local btnReset = MakeBtn("Reset Data", 270, 90)
    btnReset:SetScript("OnClick", function()
        local key  = GetCharKey()
        local name = (MythicTrackerSeasonDB[key] or {}).name or key
        StaticPopup_Show("MTSRESET_CONFIRM", name, nil, key)
    end)

    local btnExport = MakeBtn("Export", 366, 80)
    btnExport:SetScript("OnClick", ExportToChat)

    -- Legend dots (M+ only)
    local function Dot(r, g, b, label, x)
        local fs = panel:CreateFontString(nil, "OVERLAY")
        fs:SetFont("Fonts\\FRIZQT__.TTF", 11, "OUTLINE")
        fs:SetTextColor(r, g, b, 1)
        fs:SetText("• " .. label)
        fs:SetPoint("TOPLEFT", panel, "TOPLEFT", x, -53)
        table.insert(mplusControls, fs)
    end
    Dot(0.2, 0.85, 0.3,  "Timed",    460)
    Dot(0.9, 0.25, 0.25, "Depleted", 530)

    -- ── Tab switching ────────────────────────────────────────────────────
    local function SwitchTab(tab)
        activeTab = tab
        tabMplus:SetButtonState("NORMAL")
        tabLFG:SetButtonState("NORMAL")
        tabHistory:SetButtonState("NORMAL")
        if MTS_ClearHistory then MTS_ClearHistory() end
        local isMplus = (tab == "mplus")
        for _, c in ipairs(mplusControls) do c:SetShown(isMplus) end
        if tab == "mplus" then
            RefreshPanel()
        elseif tab == "lfg" then
            RefreshLFG()
        elseif tab == "history" then
            ClearRows()
            if panel.scrollChild and MTS_DrawRunHistory then
                MTS_DrawRunHistory(panel.scrollChild)
            end
        end
    end
    tabMplus:SetScript("OnClick",   function() SwitchTab("mplus")   end)
    tabLFG:SetScript("OnClick",     function() SwitchTab("lfg")     end)
    tabHistory:SetScript("OnClick", function() SwitchTab("history") end)
    panel.SwitchTab = SwitchTab

    -- ── Bottom status bar ────────────────────────────────────────────────
    local bottomBar = panel:CreateTexture(nil, "ARTWORK")
    bottomBar:SetColorTexture(0.06, 0.05, 0.02, 1)
    bottomBar:SetPoint("BOTTOMLEFT",  panel, "BOTTOMLEFT",   12,  38)
    bottomBar:SetPoint("BOTTOMRIGHT", panel, "BOTTOMRIGHT", -13,  38)
    bottomBar:SetHeight(22)

    local function BottomDot(r, g, b, label, xOff)
        local dot = panel:CreateFontString(nil, "OVERLAY")
        dot:SetFont("Fonts\\FRIZQT__.TTF", 11, "OUTLINE")
        dot:SetTextColor(r, g, b, 1)
        dot:SetText("• " .. label)
        dot:SetPoint("LEFT", bottomBar, "LEFT", xOff, 0)
        table.insert(mplusControls, dot)
    end

    BottomDot(0.2, 0.85, 0.3,  "≥80% Timed",  8)
    BottomDot(1,   0.82, 0,    "≥50% Timed",  115)
    BottomDot(0.9, 0.25, 0.25, "<50% Timed",  215)

    local ckNote = panel:CreateFontString(nil, "OVERLAY")
    ckNote:SetFont("Fonts\\FRIZQT__.TTF", 10, "OUTLINE")
    ckNote:SetTextColor(0.45, 0.45, 0.45, 1)
    ckNote:SetText("Dungeon name color reflects your timing rate")
    ckNote:SetPoint("LEFT", bottomBar, "LEFT", 320, 0)
    table.insert(mplusControls, ckNote)

    -- ── Scroll frame ─────────────────────────────────────────────────────
    local sf = CreateFrame("ScrollFrame", nil, panel, "UIPanelScrollFrameTemplate")
    sf:SetPoint("TOPLEFT",     panel, "TOPLEFT",     14,  -78)
    sf:SetPoint("BOTTOMRIGHT", panel, "BOTTOMRIGHT", -30,  64)

    local sc = CreateFrame("Frame", nil, sf)
    sc:SetWidth(700)
    sc:SetHeight(1)
    sf:SetScrollChild(sc)
    panel.scrollChild = sc
end

-- ─── Minimap Button ──────────────────────────────────────────────────────────

local pendingOpenAfterCombat = false

local function OpenPanel()
    if InCombatLockdown() then
        pendingOpenAfterCombat = true
        return
    end
    BuildPanel()
    viewMode       = "individual"
    viewSeason     = nil
    selectedAltKey = nil
    RestorePosition()
    panel:Show()
    panel:Raise()   -- bring to front in case it's behind other frames
    -- Defer the tab switch one frame so the panel is fully visible before drawing
    C_Timer.After(0, function()
        if panel.SwitchTab then
            panel.SwitchTab("mplus")
        else
            RefreshPanel()
        end
    end)
end
MTS_OpenPanel = function() OpenPanel() end  -- exposed for MythicTrackerMock.lua
MTS_RefreshPanel = function() if panel and panel:IsShown() then RefreshPanel() end end

local function BuildMinimapButton()
    if minimapBtn then return end

    -- Use LibDataBroker + LibDBIcon if available (works with MinimapButtonButton etc.)
    local LDB    = LibStub and LibStub("LibDataBroker-1.1", true)
    local DBIcon = LibStub and LibStub("LibDBIcon-1.0", true)

    if LDB and DBIcon then
        local dataObj = LDB:NewDataObject("MythicTracker", {
            type = "data source",
            text = "MythicTracker",
            icon = "Interface\\AddOns\\MythicTracker\\Icons\\Icon",
            OnClick = function(self_, button)
                if button == "LeftButton" then
                    if panel and panel:IsShown() then
                        ClosePanel()
                    else
                        OpenPanel()
                    end
                end
            end,
            OnTooltipShow = function(tooltip)
                if not tooltip then return end
                tooltip:AddLine("|cffffff00MythicTracker|r")
                tooltip:AddLine("Left-click to toggle Seasonal Runs", 1, 1, 1)
            end,
        })
        MythicTrackerSeasonDB.minimap = MythicTrackerSeasonDB.minimap or {}
        DBIcon:Register("MythicTracker", dataObj, MythicTrackerSeasonDB.minimap)
        minimapBtn = true  -- flag so we don't re-enter
        return
    end

    -- Fallback: manual button anchored to Minimap edge
    minimapBtn = CreateFrame("Button", "MythicTrackerMinimapBtn", UIParent)
    minimapBtn:SetSize(28, 28)
    minimapBtn:SetFrameStrata("MEDIUM")
    minimapBtn:SetFrameLevel(8)
    minimapBtn:SetClampedToScreen(true)

    local bg = minimapBtn:CreateTexture(nil, "BACKGROUND")
    bg:SetTexture("Interface\\Minimap\\MiniMap-TrackingBorder")
    bg:SetSize(52, 52)
    bg:SetPoint("CENTER", minimapBtn, "CENTER", 0, 0)

    local icon = minimapBtn:CreateTexture(nil, "ARTWORK")
    icon:SetTexture("Interface\\AddOns\\MythicTracker\\Icons\\Icon")
    icon:SetSize(18, 18)
    icon:SetPoint("CENTER", minimapBtn, "CENTER", 0, 0)

    local hl = minimapBtn:CreateTexture(nil, "HIGHLIGHT")
    hl:SetTexture("Interface\\Minimap\\UI-Minimap-ZoomButton-Highlight")
    hl:SetBlendMode("ADD")
    hl:SetSize(52, 52)
    hl:SetPoint("CENTER", minimapBtn, "CENTER", 0, 0)

    minimapBtn:EnableMouse(true)
    minimapBtn:RegisterForDrag("LeftButton")

    local function UpdatePos(angle)
        minimapBtn:ClearAllPoints()
        minimapBtn:SetPoint("CENTER", Minimap, "CENTER",
            math.cos(angle) * 80, math.sin(angle) * 80)
    end

    local angle = (MythicTrackerSeasonDB and MythicTrackerSeasonDB.minimapAngle) or (math.pi * 1.25)
    UpdatePos(angle)

    minimapBtn:SetScript("OnDragStart", function(self)
        self:SetScript("OnUpdate", function()
            local mx, my = Minimap:GetCenter()
            local cx, cy = GetCursorPosition()
            local scale  = UIParent:GetEffectiveScale()
            local a = math.atan2(cy/scale - my, cx/scale - mx)
            MythicTrackerSeasonDB.minimapAngle = a
            UpdatePos(a)
        end)
    end)
    minimapBtn:SetScript("OnDragStop", function(self)
        self:SetScript("OnUpdate", nil)
    end)
    minimapBtn:SetScript("OnClick", function()
        if panel and panel:IsShown() then ClosePanel() else OpenPanel() end
    end)
    minimapBtn:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, "ANCHOR_LEFT")
        GameTooltip:SetText("|cffffff00MythicTracker|r")
        GameTooltip:AddLine("Click to toggle Seasonal Runs", 0.8, 0.8, 0.8)
        GameTooltip:AddLine("Drag to reposition", 0.5, 0.5, 0.5)
        GameTooltip:Show()
    end)
    minimapBtn:SetScript("OnLeave", function() GameTooltip:Hide() end)
    minimapBtn:Show()
end

-- ─── Tab Button ──────────────────────────────────────────────────────────────

local function AnchorButton()
    if not seasonBtn then return end
    seasonBtn:ClearAllPoints()
    local tutTab = EncounterJournal and EncounterJournal.TutorialsTab
    if tutTab then
        seasonBtn:SetPoint("LEFT", tutTab, "RIGHT", 4, 0)
    else
        seasonBtn:SetPoint("BOTTOM", EncounterJournal, "BOTTOM", 220, 0)
    end
    PanelTemplates_TabResize(seasonBtn, 0)
    seasonBtn:SetButtonState("NORMAL")
    seasonBtn:Show()
end

local function InitButton()
    if seasonBtn then return end
    if not EncounterJournal then return end

    BuildPanel()

    seasonBtn = CreateFrame("Button", "MythicTrackerSeasonBtn",
        EncounterJournal, "PanelTabButtonTemplate")
    seasonBtn:SetText("Seasonal Runs")
    seasonBtn:SetFrameStrata("MEDIUM")
    seasonBtn:Hide()

    for _, key in ipairs({ "LeftActive","MiddleActive","RightActive" }) do
        if seasonBtn[key] then
            seasonBtn[key]:Hide()
            seasonBtn[key].Show = function() end
        end
    end

    seasonBtn:SetScript("OnClick", function()
        if panel:IsShown() then
            ClosePanel()
        else
            viewMode   = "individual"
            viewSeason = nil
            RestorePosition()
            panel:Show()
            if panel.SwitchTab then
                panel.SwitchTab("mplus")
            else
                RefreshPanel()
            end
            if EncounterJournal and EncounterJournal:IsShown() then
                HideUIPanel(EncounterJournal)
            end
        end
    end)

    EncounterJournal:HookScript("OnShow", function()
        C_Timer.After(0.1, AnchorButton)
    end)
    EncounterJournal:HookScript("OnHide", function()
        seasonBtn:Hide()
        if not panel:IsShown() then ClosePanel() end
    end)

    if EncounterJournal:IsShown() then
        C_Timer.After(0.1, AnchorButton)
    end
end

-- ─── Events ──────────────────────────────────────────────────────────────────

local f = CreateFrame("Frame")
f:RegisterEvent("ADDON_LOADED")
f:RegisterEvent("CHALLENGE_MODE_START")
f:RegisterEvent("CHALLENGE_MODE_COMPLETED")
f:RegisterEvent("PLAYER_ENTERING_WORLD")
f:RegisterEvent("PLAYER_LEVEL_UP")
f:RegisterEvent("PLAYER_SPECIALIZATION_CHANGED")
f:RegisterEvent("LFG_LIST_APPLICATION_STATUS_UPDATED")
f:RegisterEvent("ZONE_CHANGED_NEW_AREA")
f:RegisterEvent("PLAYER_REGEN_ENABLED")
f:RegisterEvent("CHAT_MSG_SYSTEM")
f:RegisterEvent("UI_INFO_MESSAGE")
f:RegisterEvent("CHAT_MSG_RAID_BOSS_EMOTE")

f:SetScript("OnEvent", function(_, event, arg1, arg2, arg3, arg4)
    if event == "ADDON_LOADED" then
        if arg1 == ADDON_NAME then
            EnsureDB()
            if IsEligibleLevel() then GetCharDB(GetCharKey()) end
            -- Build minimap button after saved vars are loaded
            C_Timer.After(0, BuildMinimapButton)
        end
        if arg1 == "Blizzard_EncounterJournal" then
            C_Timer.After(0.1, InitButton)
        end

    elseif event == "PLAYER_ENTERING_WORLD" then
        if IsEligibleLevel() then GetCharDB(GetCharKey()) end
        if EncounterJournal then C_Timer.After(0.5, InitButton) end
        if ToggleEncounterJournal and not f._hookedToggle then
            f._hookedToggle = true
            hooksecurefunc("ToggleEncounterJournal", function()
                C_Timer.After(0.1, InitButton)
            end)
        end
        -- Hook vault open so we can mark it as "opened this week"
        C_Timer.After(2, function()
            if WeeklyRewardsFrame and not f._hookedVault then
                f._hookedVault = true
                WeeklyRewardsFrame:HookScript("OnShow", function()
                    local cdb = GetCharDB(GetCharKey())
                    cdb.vaultOpenedWeek = GetWeekID()
                end)
            end
        end)

    elseif event == "PLAYER_REGEN_ENABLED" then
        if pendingOpenAfterCombat then
            pendingOpenAfterCombat = false
            OpenPanel()
        end

    elseif event == "PLAYER_LEVEL_UP" then
        GetCharDB(GetCharKey())

    elseif event == "PLAYER_SPECIALIZATION_CHANGED" then
        GetCharDB(GetCharKey())

    elseif event == "CHAT_MSG_SYSTEM" or event == "UI_INFO_MESSAGE" or event == "CHAT_MSG_RAID_BOSS_EMOTE" then
        local msg = arg1
        if msg then
            -- Match "(Level 10)" in any system message
            local lv = msg:match("%(Level (%d+)%)")
                     or msg:match("Level (%d+)%)")
                     or msg:match("Level (%d+) ")
            if lv then
                lastCompletedLevel = tonumber(lv)
            end
        end

    elseif event == "ZONE_CHANGED_NEW_AREA" then
        -- Capture instance name and keystone level when zoning in.
        -- The keystone is still in bags at this point — before the Font of Power consumes it.
        pcall(function()
            local iName = GetInstanceInfo and select(1, GetInstanceInfo())
            if iName and iName ~= "" and iName ~= "Unknown" then
                lastInstanceName = iName
            end
        end)
        pcall(function()
            -- Scan bags for the keystone item and parse level from link
            -- Link format: ...|H item:itemID:...|h[Keystone: DungeonName]|h...
            -- Level is encoded in the item link bonus IDs, but we can also use GetOwnedKeystoneLevel
            -- here since the key hasn't been inserted into the Font yet
            local lv = C_MythicPlus and C_MythicPlus.GetOwnedKeystoneLevel
                       and C_MythicPlus.GetOwnedKeystoneLevel()
            if lv and lv > 0 then
                lastKeystoneLevel = lv
            end
        end)

    elseif event == "LFG_LIST_APPLICATION_STATUS_UPDATED" then
        if not IsEligibleLevel() then return end
        local appID = arg1
        if not appID then return end

        -- In Midnight the status may come from GetApplicationInfo or directly as arg2
        local status = nil
        local searchResultID = nil
        pcall(function()
            local appInfo = C_LFGList and C_LFGList.GetApplicationInfo and
                            C_LFGList.GetApplicationInfo(appID)
            if appInfo then
                status = appInfo.status
                searchResultID = appInfo.searchResultID
            end
        end)
        -- Some Midnight builds pass status directly as arg2
        if not status and type(arg2) == "string" then
            status = arg2
        end
        if not status then return end

        -- Normalise to lowercase for comparison
        status = status:lower()
        -- Only count an explicit leader decline — NOT "declined_delisted" (group disbanded),
        -- "declined_full", "declined_delisted" etc. Those are not rejections of you personally.
        local isDeclined = status == "declined"
        local isInvited  = status == "invited" or status == "accepted"

        if not isDeclined and not isInvited then return end
        local recordStatus = isDeclined and "declined" or "invited"

        -- Get dungeon name
        local dungeonName = nil
        if searchResultID and C_LFGList and C_LFGList.GetSearchResultInfo then
            pcall(function()
                local resultInfo = C_LFGList.GetSearchResultInfo(searchResultID)
                if resultInfo and resultInfo.activityID and C_LFGList.GetActivityInfo then
                    local actName = C_LFGList.GetActivityInfo(resultInfo.activityID)
                    if type(actName) == "string" and actName ~= "" then
                        dungeonName = actName
                    end
                end
            end)
        end

        RecordLFG(recordStatus, dungeonName)
        if panel and panel:IsShown() and activeTab == "lfg" then
            RefreshLFG()
        end

    elseif event == "CHALLENGE_MODE_START" then
        f._debugChatUntil = time() + 300  -- print system messages for 5 mins
        pendingDungeon = nil
        pendingLevel   = nil
        pendingAffixes = {}
        pendingGroup   = nil

        local function TryCapture()
            -- GetInstanceInfo is most reliable inside the instance
            pcall(function()
                local iName = GetInstanceInfo and select(1, GetInstanceInfo())
                if iName and iName ~= "" and iName ~= "Unknown" then
                    pendingDungeon = iName
                    lastInstanceName = iName
                end
            end)
            if not pendingDungeon or pendingDungeon == "" then
                local name = ResolveDungeonName()
                if name and name ~= "" then pendingDungeon = name end
            end

            -- GetSlottedKeystoneInfo returns the level of the key IN the Font of Power right now
            if not pendingLevel or pendingLevel == 0 then
                pcall(function()
                    if C_ChallengeMode and C_ChallengeMode.GetSlottedKeystoneInfo then
                        local mapID, affixIDs, lv = C_ChallengeMode.GetSlottedKeystoneInfo()
                        if lv and lv > 0 then pendingLevel = lv end
                    end
                end)
            end
            -- GetActiveKeystoneInfo: activeKeystoneLevel, activeAffixIDs, wasActiveKeystoneCharged
            if not pendingLevel or pendingLevel == 0 then
                pcall(function()
                    if C_ChallengeMode and C_ChallengeMode.GetActiveKeystoneInfo then
                        local lv = C_ChallengeMode.GetActiveKeystoneInfo()
                        if lv and lv > 0 then pendingLevel = lv end
                    end
                end)
            end

            if not pendingAffixes or #pendingAffixes == 0 then
                pendingAffixes = GetCurrentAffixes()
            end
            if not pendingGroup then
                pendingGroup = CaptureGroupSnapshot()
            end
        end

        TryCapture()
        C_Timer.After(0.5, TryCapture)
        C_Timer.After(2.0, TryCapture)

    elseif event == "CHALLENGE_MODE_COMPLETED" then
        if not IsEligibleLevel() then return end

        -- CHALLENGE_MODE_COMPLETED fires with NO arguments (wiki confirmed: "None")
        -- All level/timed data must come from APIs

        -- Dungeon name
        local nameNow = pendingDungeon
        if not nameNow or nameNow == "" then
            pcall(function()
                local iName = GetInstanceInfo and select(1, GetInstanceInfo())
                if iName and iName ~= "" and iName ~= "Unknown" then nameNow = iName end
            end)
        end
        if not nameNow or nameNow == "" then nameNow = lastInstanceName end

        -- Level: GetSlottedKeystoneInfo still works at completion time
        local levelToRecord = pendingLevel or 0
        if levelToRecord == 0 then
            pcall(function()
                if C_ChallengeMode and C_ChallengeMode.GetSlottedKeystoneInfo then
                    local _, _, lv = C_ChallengeMode.GetSlottedKeystoneInfo()
                    if lv and lv > 0 then levelToRecord = lv end
                end
            end)
        end
        if levelToRecord == 0 then
            pcall(function()
                if C_ChallengeMode and C_ChallengeMode.GetActiveKeystoneInfo then
                    local lv = C_ChallengeMode.GetActiveKeystoneInfo()
                    if lv and lv > 0 then levelToRecord = lv end
                end
            end)
        end
        -- lastCompletedLevel from CHAT_MSG_SYSTEM if it fired
        if levelToRecord == 0 and lastCompletedLevel and lastCompletedLevel > 0 then
            levelToRecord = lastCompletedLevel
        end

        local snapAffixes = pendingAffixes or {}
        pendingDungeon, pendingLevel, pendingAffixes, pendingGroup = nil, nil, nil, nil
        lastCompletedLevel = nil
        lastKeystoneLevel  = nil

        C_Timer.After(1.5, function()
            local onTime = false
            pcall(function()
                if C_MythicPlus and C_MythicPlus.GetRunHistory then
                    local h = C_MythicPlus.GetRunHistory(false, false)
                    if h and h[1] then
                        onTime = h[1].completed or h[1].onTime or false
                        -- Last resort for level
                        if levelToRecord == 0 and h[1].level and h[1].level > 0 then
                            levelToRecord = h[1].level
                        end
                    end
                end
            end)
            local finalName = StripColors(nameNow) or "Unknown"
            RecordRun(finalName, onTime, levelToRecord, snapAffixes)
        end)
    end
end)

-- ─── Debug ───────────────────────────────────────────────────────────────────

-- ─── Slash Commands ──────────────────────────────────────────────────────────

SLASH_MTS1 = "/mts"
SlashCmdList["MTS"] = function(msg)
    local cmd = (msg or ""):lower():match("^%s*(%S*)")
    if cmd == "" or cmd == "open" then
        OpenPanel()
    elseif cmd == "clearhistory" then
        if MythicTrackerSeasonDB then
            MythicTrackerSeasonDB.runHistory = nil
            print("|cffff9900[MythicTracker]|r Run history cleared.")
            if panel and panel:IsShown() and activeTab == "history" then
                ClearRows()
                if panel.scrollChild and MTS_DrawRunHistory then
                    MTS_DrawRunHistory(panel.scrollChild)
                end
            end
        end
    else
        print("|cffffff00/mts|r — open the MythicTracker panel")
        print("|cffffff00/mts clearhistory|r — wipe all M+ run history")
    end
end

SLASH_MTSDEBUG1 = "/mtsdebug"
SlashCmdList["MTSDEBUG"] = function()
    local p = "|cffff9900[MTS]|r"
    print(p, "Char:", GetCharKey(), "Lv:", UnitLevel("player"), "Eligible:", IsEligibleLevel())
    print(p, "EJ:", EncounterJournal ~= nil, "TutorialsTab:", EncounterJournal and EncounterJournal.TutorialsTab ~= nil)
    print(p, "seasonBtn:", seasonBtn ~= nil, "panel:", panel ~= nil, "minimapBtn:", minimapBtn ~= nil)
    InitButton()
    if seasonBtn then AnchorButton() end
    BuildMinimapButton()
    print(p, "|cff00ff00Done.|r")
end