-- ─────────────────────────────────────────────────────────────────────────────
-- Keystone Trust Addon
-- Démarre/arrête le combat log lors des runs M+ et sauvegarde les métadonnées.
-- Le parsing des stats est fait par le client KT desktop via WoWCombatLog.txt.
-- ─────────────────────────────────────────────────────────────────────────────

local ADDON_NAME    = "KeystoneTrust"
local ADDON_VERSION = "0.5.53"

-- ── Structures ────────────────────────────────────────────────────────────────
KeystoneTrustDB = KeystoneTrustDB or {}
KT_BENCHMARKS   = KT_BENCHMARKS or {}

local KT = {
    run          = nil,
    inMythicPlus = false,
    runStartTime = nil,
    myKey        = nil,
    frame        = CreateFrame("Frame"),
}

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

local function GetRunElapsed()
    if not KT.runStartTime then return 0 end
    return math.floor(GetServerTime() - KT.runStartTime)
end

local function GetPlayerRealm()
    local name, realm = UnitFullName("player")
    return name or UnitName("player"), realm or GetRealmName()
end

local function DetectRegion()
    local REGION_MAP = {[1]="us",[2]="kr",[3]="eu",[4]="tw",[5]="cn"}
    if GetCurrentRegion then
        local id = GetCurrentRegion()
        if id and REGION_MAP[id] then return REGION_MAP[id] end
    end
    if GetCurrentRegionName then
        local n = GetCurrentRegionName():lower()
        if n:find("americ") or n:find("^us") then return "us" end
        if n:find("^eu")                      then return "eu" end
        if n:find("korea")  or n:find("^kr")  then return "kr" end
        if n:find("taiwan") or n:find("^tw")  then return "tw" end
        if n:find("china")  or n:find("^cn")  then return "cn" end
    end
    return "eu"
end

local function GetSpecName()
    local idx = GetSpecialization()
    if not idx then return "Unknown" end
    return select(2, GetSpecializationInfo(idx)) or "Unknown"
end

local function GetSpecId()
    local idx = GetSpecialization()
    if not idx then return 0 end
    return (GetSpecializationInfo(idx)) or 0
end

local function GetRoleFromSpec()
    local idx = GetSpecialization()
    if not idx then return "DPS" end
    local role = select(5, GetSpecializationInfo(idx))
    if role == "TANK" then return "TANK"
    elseif role == "HEALER" then return "HEALER"
    else return "DPS" end
end

local function GetIlvl()
    local total, count = 0, 0
    for i = 1, 17 do
        if i ~= 4 then
            local link = GetInventoryItemLink("player", i)
            if link then
                local ilvl = select(4, GetItemInfo(link))
                if ilvl and ilvl > 0 then total = total + ilvl; count = count + 1 end
            end
        end
    end
    return count > 0 and math.floor(total / count) or 0
end

-- ── Joueur (métadonnées uniquement — stats parsées par le client KT) ──────────

local function NewPlayer(name, realm, guid)
    return {
        name   = name,
        realm  = realm or GetRealmName(),
        guid   = guid or "",
        class  = "UNKNOWN",
        spec   = "",
        specId = 0,
        role   = "",
        ilvl   = 0,
    }
end

local function GetOrCreatePartyMember(guid, name, realm)
    if not KT.run then return nil end
    if not KT.run.party[guid] then
        local p = NewPlayer(name, realm, guid)
        for _, unit in ipairs({"player","party1","party2","party3","party4"}) do
            if UnitExists(unit) and UnitGUID(unit) == guid then
                local _, cf = UnitClass(unit)
                if cf then p.class = cf end
                local wowRole = UnitGroupRolesAssigned(unit) or ""
                if wowRole == "DAMAGER" then wowRole = "DPS" end
                p.role = wowRole
                break
            end
        end
        KT.run.party[guid] = p
    end
    return KT.run.party[guid]
end

-- ── Run lifecycle ─────────────────────────────────────────────────────────────

local function StartRun(dungeonName, keystoneLevel, affixes, dungeonId)
    KT.runStartTime = GetServerTime()
    KT.inMythicPlus = true

    local playerName, realm = GetPlayerRealm()
    local _, classFile = UnitClass("player")

    KT.run = {
        addonVersion   = ADDON_VERSION,
        playerName     = playerName,
        realm          = realm,
        region         = DetectRegion(),
        wowClass       = classFile or "UNKNOWN",
        spec           = GetSpecName(),
        specId         = GetSpecId(),
        role           = GetRoleFromSpec(),
        dungeonId      = dungeonId or 0,
        dungeonName    = dungeonName,
        keystoneLevel  = keystoneLevel,
        affixes        = affixes or {},
        completed      = false,
        inTime         = false,
        durationSecs   = 0,
        timeLimitSecs  = 0,
        runDate        = date("!%Y-%m-%dT%H:%M:%SZ"),
        party          = {},
        startedLogging = false,
    }

    if C_ChallengeMode.GetTimeLimit then
        KT.run.timeLimitSecs = C_ChallengeMode.GetTimeLimit() or 0
    end

    -- Joueur principal
    local mainGuid = UnitGUID("player")
    local main = NewPlayer(playerName, realm, mainGuid)
    main.class  = classFile or "UNKNOWN"
    main.spec   = GetSpecName()
    main.specId = GetSpecId()
    main.role   = GetRoleFromSpec()
    main.ilvl   = GetIlvl()
    KT.run.party[mainGuid] = main

    -- Membres déjà présents dans le groupe
    for i = 1, 4 do
        local unit = "party" .. i
        if UnitExists(unit) then
            local n, r = UnitFullName(unit)
            local g    = UnitGUID(unit)
            if g and n then GetOrCreatePartyMember(g, n, r) end
        end
    end

    -- Activer le combat log pour que le client KT puisse parser les stats
    if not LoggingCombat() then
        LoggingCombat(true)
        KT.run.startedLogging = true
    end

    print("|cff00ccff[KT]|r Run M+ démarré : |cffffcc00" .. dungeonName .. " +" .. keystoneLevel ..
          "|r — combat log " .. (KT.run.startedLogging and "activé" or "déjà actif"))
end

local function FinalizeRun(completed, inTime, durationSecs)
    if not KT.run then return end

    KT.run.completed    = completed
    KT.run.inTime       = inTime
    KT.run.durationSecs = durationSecs
    local startedLogging = KT.run.startedLogging

    -- Mettre à jour les infos du joueur principal
    local mainGuid = UnitGUID("player")
    if KT.run.party[mainGuid] then
        KT.run.party[mainGuid].spec   = GetSpecName()
        KT.run.party[mainGuid].specId = GetSpecId()
        KT.run.party[mainGuid].role   = GetRoleFromSpec()
        KT.run.party[mainGuid].ilvl   = GetIlvl()
    end

    -- Convertir le dict party en tableau
    local partyArray = {}
    for _, p in pairs(KT.run.party) do
        table.insert(partyArray, p)
    end
    KT.run.party = partyArray

    -- Sauvegarder dans SavedVariables (le client desktop lit WoWCombatLog.txt directement)
    local runId = KT.run.playerName .. "_" ..
                  KT.run.dungeonName:gsub("[^%w]","") .. "_" ..
                  GetServerTime()
    KeystoneTrustDB.runs        = KeystoneTrustDB.runs or {}
    KeystoneTrustDB.runs[runId] = KT.run

    local result = inTime and "|cff00ff00Dans les temps ✓|r" or "|cffff4444Hors temps|r"
    print("|cff00ccff[KT]|r Run terminé " .. result .. " | données sauvegardées pour upload.")

    KT.run          = nil
    KT.inMythicPlus = false
    KT.runStartTime = nil

    -- Désactiver le combat log avec délai (joueurs peuvent sortir pour réparer)
    if startedLogging then
        local delay = completed and 10 or 30
        C_Timer.After(delay, function() LoggingCombat(false) end)
    end
end

-- ── Benchmark & Tooltip ───────────────────────────────────────────────────────

local TOOLTIP_RANK_COLOR = {
    GLADIATEUR="FF8000", ELITE="FF4500", MASTER="C084FC",
    DIAMANT="818CF8",    EMERAUDE="00C0A0", PLATINE="22D3EE",
    OR="FACC15",         ARGENT="D1D5DB",   BRONZE="B45309", PEON="9CA3AF",
}
local TOOLTIP_RANK_LABEL = {
    GLADIATEUR="Gladiateur", ELITE="Élite",     MASTER="Master",
    DIAMANT="Diamant",       EMERAUDE="Émeraude", PLATINE="Platine",
    OR="Or",                 ARGENT="Argent",     BRONZE="Bronze", PEON="Péon",
}

local function PctToRank(pct)
    if not pct         then return "PEON" end
    if     pct >= 99.9 then return "GLADIATEUR"
    elseif pct >= 99.7 then return "ELITE"
    elseif pct >= 99   then return "MASTER"
    elseif pct >= 96.7 then return "DIAMANT"
    elseif pct >= 83   then return "EMERAUDE"
    elseif pct >= 64   then return "PLATINE"
    elseif pct >= 45   then return "OR"
    elseif pct >= 27   then return "ARGENT"
    elseif pct >= 8    then return "BRONZE"
    else                    return "PEON" end
end

local function FmtK(v)
    if not v then return "—" end
    if v >= 1000000 then return string.format("%.1fM", v/1000000)
    elseif v >= 1000 then return string.format("%dk", math.floor(v/1000))
    else return tostring(math.floor(v)) end
end

local function NormalizeRealm(realm)
    if not realm then return "" end
    local s = realm:lower()
    -- Strip NFC diacritics (UTF-8 precomposed: é=\xc3\xa9, à=\xc3\xa0 …)
    s = s:gsub("\xc3\xa9","e"):gsub("\xc3\xa8","e"):gsub("\xc3\xaa","e"):gsub("\xc3\xab","e")
    s = s:gsub("\xc3\xa0","a"):gsub("\xc3\xa2","a"):gsub("\xc3\xa4","a"):gsub("\xc3\xa1","a")
    s = s:gsub("\xc3\xb9","u"):gsub("\xc3\xbb","u"):gsub("\xc3\xbc","u"):gsub("\xc3\xba","u")
    s = s:gsub("\xc3\xae","i"):gsub("\xc3\xaf","i"):gsub("\xc3\xad","i")
    s = s:gsub("\xc3\xb4","o"):gsub("\xc3\xb6","o"):gsub("\xc3\xb3","o")
    s = s:gsub("\xc3\xa7","c"):gsub("\xc3\xb1","n")
    -- Strip NFD combining marks (\xcc\x80-\xcc\xbf and \xcd\x80-\xcd\xaf)
    s = s:gsub("\xcc[\x80-\xbf]",""):gsub("\xcd[\x80-\xaf]","")
    s = s:gsub("'",""):gsub("%s+","-")
    return s
end

local function GetKTCache(nameRealm)
    if not KT_BENCHMARKS then return nil end
    return KT_BENCHMARKS[nameRealm]
end

local function GetKTCacheUnit(name, realm)
    if not name then return nil end
    local realmNorm = NormalizeRealm(realm)
    -- Benchmark file uses lowercase names — try exact case first, then lowercase fallback.
    return KT_BENCHMARKS[name .. "-" .. realmNorm]
        or KT_BENCHMARKS[name:lower() .. "-" .. realmNorm]
end

local function GetDungeonsForUnit(data, unit)
    if not data or not data.roles then return nil, nil, false end
    local wowRole = UnitGroupRolesAssigned and UnitGroupRolesAssigned(unit) or ""
    local roleKey
    if wowRole == "TANK"        then roleKey = "TANK"
    elseif wowRole == "HEALER"  then roleKey = "HEALER"
    elseif wowRole == "DAMAGER" then roleKey = "DPS"
    end
    if roleKey then
        local dungeons = data.roles[roleKey]
        if dungeons and #dungeons > 0 then return dungeons, roleKey, (roleKey == "HEALER") end
        return {}, roleKey, (roleKey == "HEALER")
    end
    for _, rk in ipairs({"DPS","HEALER","TANK"}) do
        if data.roles[rk] and #data.roles[rk] > 0 then
            return data.roles[rk], rk, (rk == "HEALER")
        end
    end
    return nil, nil, false
end

local function RenderKTDungeons(dungeons, roleKey, isHealer, showTooltip)
    if not dungeons or #dungeons == 0 then return end
    if showTooltip == nil then showTooltip = true end
    local roleLabel = roleKey == "HEALER" and "Heal" or roleKey == "TANK" and "Tank" or "DPS"
    GameTooltip:AddLine(" ")
    GameTooltip:AddLine(string.format("|cff00ccff[Keystone Trust]|r |cff94a3b8— %s|r", roleLabel))
    local perfHeader = isHealer and "HPS" or "DPS"
    GameTooltip:AddDoubleLine(
        "|cff6b7280Dungeon|r",
        string.format("|cff6b7280Rank         top    %s    kick   death|r", perfHeader),
        1, 1, 1, 1, 1, 1)
    for _, d in ipairs(dungeons) do
        local perf   = isHealer and (FmtK(d.hps) .. " hps") or (FmtK(d.dps) .. " dps")
        local kicks  = d.kicks  ~= nil and tostring(d.kicks)  or "—"
        local deaths = d.deaths ~= nil and tostring(d.deaths) or "—"
        local left   = string.format("|cffffcc00+%s|r %s", tostring(d.level or "?"), d.name or "?")
        local rank   = d.rank
        local color  = rank and (TOOLTIP_RANK_COLOR[rank] or "9CA3AF") or "6B7280"
        local label  = rank and (TOOLTIP_RANK_LABEL[rank] or rank)     or "?"
        local pct    = d.topPct and ("top " .. tostring(d.topPct) .. "%") or "?%"
        local right  = string.format("|cff%s[%s]|r %s  %s  %sk %sm",
                           color, label, pct, perf, kicks, deaths)
        GameTooltip:AddDoubleLine(left, right, 0.8,0.8,0.8, 1,1,1)
    end
    -- showTooltip=false when called from TooltipDataProcessor (fires before Show, no resize needed)
    -- showTooltip=true when called from OnShow deferred (tooltip already shown, resize needed)
    if showTooltip then GameTooltip:Show() end
end

-- Unit-based tooltip (party frames, nameplates, raid frames…)
-- showTooltip=false when called from TooltipDataProcessor (before Show), true otherwise
local function OnTooltipSetUnit(tooltip, data, showTooltip)
    if showTooltip == nil then showTooltip = true end
    local unit = (data and data.unit) or select(2, tooltip:GetUnit())
    if not unit then return end
    -- UnitIsPlayer can error with "secret values" on world-cursor units (SetWorldCursor).
    local ok, isPlayer = pcall(UnitIsPlayer, unit)
    if not ok or not isPlayer then return end
    local name, realm = UnitFullName(unit)
    if not name then return end
    realm = realm or GetRealmName()
    local ktData = GetKTCacheUnit(name, realm)
    if not ktData then
        tooltip:AddLine(" ")
        tooltip:AddLine("|cff00ccff[KT]|r Non analysé sur wowkeystonetrust.com")
        if showTooltip then tooltip:Show() end
        return
    end
    local dungeons, roleKey, isHealer = GetDungeonsForUnit(ktData, unit)
    if not dungeons then return end
    if #dungeons == 0 then
        local roleLabel = roleKey == "HEALER" and "Heal" or roleKey == "TANK" and "Tank" or "DPS"
        tooltip:AddLine(" ")
        tooltip:AddLine(string.format("|cff00ccff[Keystone Trust]|r |cff94a3b8— %s|r", roleLabel))
        tooltip:AddLine("|cff6b7280No data for this spec|r")
        if showTooltip then tooltip:Show() end
        return
    end
    RenderKTDungeons(dungeons, roleKey, isHealer, showTooltip)
end

-- Name-based tooltip (friends list, LFG group finder…)
-- showNotFound: affiche un message si le joueur n'est pas dans KT_BENCHMARKS
local function TryKTTooltipForName(nameRealm, showNotFound)
    if not nameRealm or nameRealm == "" then return end
    -- Split "Name-Realm" and normalize the realm so the key matches KT_BENCHMARKS slugs
    local pname, prealm = nameRealm:match("^([^%-]+)%-(.+)$")
    local lookupKey = (pname and prealm) and (pname .. "-" .. NormalizeRealm(prealm)) or nameRealm
    local data = GetKTCache(lookupKey)
    if not data and pname and prealm then
        data = GetKTCache(pname:lower() .. "-" .. NormalizeRealm(prealm))
    end
    if not data and lookupKey ~= nameRealm then
        data = GetKTCache(nameRealm)  -- fallback: try original casing
    end
    if not data then
        if not nameRealm:find("%-") then
            data = GetKTCache(nameRealm .. "-" .. NormalizeRealm(GetRealmName()))
            if not data then
                local lower = nameRealm:lower()
                for key, val in pairs(KT_BENCHMARKS) do
                    if key:lower():match("^" .. lower .. "%-") then
                        data = val
                        break
                    end
                end
            end
        end
    end
    if not data or not data.roles then
        if showNotFound then
            GameTooltip:AddLine(" ")
            GameTooltip:AddLine("|cff00ccff[KT]|r Non analysé sur wowkeystonetrust.com")
            GameTooltip:Show()
        end
        return
    end
    local dungeons, roleKey, isHealer
    for _, rk in ipairs({"DPS", "HEALER", "TANK"}) do
        if data.roles[rk] and #data.roles[rk] > 0 then
            dungeons, roleKey, isHealer = data.roles[rk], rk, (rk == "HEALER")
            break
        end
    end
    if not dungeons then return end
    RenderKTDungeons(dungeons, roleKey, isHealer)
end

-- ── Event frame ───────────────────────────────────────────────────────────────

KT.frame:RegisterEvent("ADDON_LOADED")
KT.frame:RegisterEvent("CHALLENGE_MODE_START")
KT.frame:RegisterEvent("CHALLENGE_MODE_COMPLETED")
KT.frame:RegisterEvent("CHALLENGE_MODE_RESET")
KT.frame:RegisterEvent("GROUP_ROSTER_UPDATE")
KT.frame:RegisterEvent("ZONE_CHANGED_NEW_AREA")

KT.frame:SetScript("OnEvent", function(_, event, ...)
    if event == "ADDON_LOADED" then
        local name = ...
        if name ~= ADDON_NAME then return end
        KeystoneTrustDB.runs = KeystoneTrustDB.runs or {}
        local myName, myRealm = GetPlayerRealm()
        KT.myKey = myName .. "-" .. myRealm
        local ncached = 0
        for _ in pairs(KT_BENCHMARKS) do ncached = ncached + 1 end
        print("|cff00ccff[Keystone Trust]|r v" .. ADDON_VERSION ..
              " chargé — " .. ncached .. " profil(s) KT en cache (/kt bench)")
        -- Unit-based (party frames, nameplates…)
        -- TooltipDataProcessor fires before GameTooltip:Show(), so no resize flicker.
        if TooltipDataProcessor and TooltipDataProcessor.AddTooltipPostCall and Enum.TooltipDataType then
            TooltipDataProcessor.AddTooltipPostCall(Enum.TooltipDataType.Unit, function(tooltip, data)
                if tooltip ~= GameTooltip then return end
                OnTooltipSetUnit(tooltip, data, false)
            end)
        else
            -- Fallback for environments where TooltipDataProcessor is unavailable
            hooksecurefunc(GameTooltip, "SetUnit", function(tt, unit)
                if not unit then return end
                C_Timer.After(0, function()
                    if not GameTooltip:IsShown() then return end
                    local _, cur = GameTooltip:GetUnit()
                    if cur == unit then OnTooltipSetUnit(GameTooltip, { unit = unit }, true) end
                end)
            end)
        end

        -- Name-based (friends list, LFG…)
        -- Deferred to next frame so all addons (Raider.IO etc.) have added their lines first.
        GameTooltip:HookScript("OnShow", function(tt)
            C_Timer.After(0, function()
                if not GameTooltip:IsShown() then return end
                local _, unit = GameTooltip:GetUnit()
                if unit then return end -- already handled by TooltipDataProcessor
                -- pcall: tooltip text from LFG/protected frames is "secret" and
                -- cannot be compared directly — taint would cause a Lua error.
                pcall(function()
                    for i = 1, 5 do
                        local line = _G["GameTooltipTextLeft" .. i]
                        if not line then break end
                        local raw = line:GetText()
                        if not raw or raw == "" then break end
                        local name = raw:gsub("|c%x%x%x%x%x%x%x%x", ""):gsub("|r", ""):match("^%s*(.-)%s*$")
                        if name and name ~= "" and not name:find("%s") and #name >= 2 and #name <= 30 then
                            if KT._debugtt then
                                print("|cff00ccff[KT DEBUG]|r OnShow(deferred) line" .. i .. "='" .. name .. "'")
                            end
                            TryKTTooltipForName(name, name:find("%-") ~= nil)
                            return
                        end
                    end
                end)
            end)
        end)

    elseif event == "CHALLENGE_MODE_START" then
        -- Active le combat log automatiquement dès qu'une clé commence.
        -- Le joueur n'a plus besoin d'un addon tiers (WilduTools, etc.) pour ça.
        if not LoggingCombat() then
            LoggingCombat(true)
            print("|cff7EC8E3[KeystoneTrust]|r Combat log activé automatiquement.")
        end
        C_Timer.After(0.5, function()
            -- Noms canoniques anglais par mapID — indépendant de la locale du client.
            -- Ajouter les autres mapID au fur et à mesure des runs confirmés.
            local KT_DUNGEON_CANONICAL = {
                [161] = "Skyreach",
                [239] = "Seat of the Triumvirate",
                [402] = "Algeth'ar Academy",
                [556] = "Pit of Saron",
                [557] = "Windrunner Spire",
                [558] = "Magisters' Terrace",
                [559] = "Nexus-Point Xenas",
                [560] = "Maisara Caverns",
            }
            local mapID = C_ChallengeMode.GetActiveChallengeMapID
                          and C_ChallengeMode.GetActiveChallengeMapID()
            local mapName = mapID and KT_DUNGEON_CANONICAL[mapID]
            if not mapName then
                mapName = GetRealZoneText() or "Unknown Dungeon"
                if mapID and C_ChallengeMode.GetMapUIInfo then
                    local n = C_ChallengeMode.GetMapUIInfo(mapID)
                    if n and n ~= "" then mapName = n end
                end
            end
            local keystoneLevel = 0
            if C_ChallengeMode.GetActiveKeystoneInfo then
                local lvl = C_ChallengeMode.GetActiveKeystoneInfo()
                keystoneLevel = lvl or 0
            end
            if keystoneLevel == 0 and C_ChallengeMode.GetActiveKeystoneLevel then
                keystoneLevel = C_ChallengeMode.GetActiveKeystoneLevel() or 0
            end
            local affixList = {}
            local affixIds = (C_ChallengeMode.GetActiveKeystoneAffixes
                             and C_ChallengeMode.GetActiveKeystoneAffixes()) or {}
            if #affixIds == 0 and C_ChallengeMode.GetActiveKeystoneInfo then
                local _, ids = C_ChallengeMode.GetActiveKeystoneInfo()
                affixIds = ids or {}
            end
            for _, id in ipairs(affixIds) do
                local affixName = C_ChallengeMode.GetAffixInfo
                                  and select(1, C_ChallengeMode.GetAffixInfo(id))
                table.insert(affixList, affixName or tostring(id))
            end
            StartRun(mapName, keystoneLevel, affixList, mapID)
        end)

    elseif event == "CHALLENGE_MODE_COMPLETED" then
        local _, _, timeMs, onTime = ...
        local elapsedSecs = math.floor((timeMs or GetRunElapsed() * 1000) / 1000)
        local inTime
        if KT.run and KT.run.timeLimitSecs and KT.run.timeLimitSecs > 0 then
            inTime = elapsedSecs <= KT.run.timeLimitSecs
        else
            inTime = (onTime == true or onTime == 1)
        end
        FinalizeRun(true, inTime, elapsedSecs)

    elseif event == "CHALLENGE_MODE_RESET" then
        if KT.inMythicPlus and KT.run then
            FinalizeRun(false, false, GetRunElapsed())
        end

    elseif event == "ZONE_CHANGED_NEW_AREA" then
        -- Activer le log dès l'entrée dans une instance M+ (avant CHALLENGE_MODE_START).
        -- C_ChallengeMode.GetActiveChallengeMapID retourne un mapID non-nil dès qu'on
        -- est dans une zone mythique+ valide, même avant que la clé ne soit lancée.
        local inMythicZone = C_ChallengeMode.GetActiveChallengeMapID
                             and C_ChallengeMode.GetActiveChallengeMapID() ~= nil
                             and C_ChallengeMode.GetActiveChallengeMapID() ~= 0
        if inMythicZone and not LoggingCombat() then
            LoggingCombat(true)
            print("|cff7EC8E3[KeystoneTrust]|r Combat log activé (entrée en instance M+).")
        end

    elseif event == "GROUP_ROSTER_UPDATE" then
        if not KT.inMythicPlus or not KT.run then return end
        for i = 1, 4 do
            local unit = "party" .. i
            if UnitExists(unit) then
                local n, r = UnitFullName(unit)
                local g    = UnitGUID(unit)
                if g and n and not KT.run.party[g] then
                    GetOrCreatePartyMember(g, n, r)
                end
            end
        end
    end
end)

-- ── Slash commands ────────────────────────────────────────────────────────────

SLASH_KT1 = "/kt"
SlashCmdList["KT"] = function(msg)
    local cmd = strtrim(msg):lower()

    if cmd == "status" then
        if KT.inMythicPlus and KT.run then
            print("|cff00ccff[KT]|r " .. KT.run.dungeonName .. " +" ..
                  KT.run.keystoneLevel .. " | " .. GetRunElapsed() .. "s" ..
                  " | logging=" .. tostring(LoggingCombat()))
        else
            print("|cff00ccff[KT]|r Aucun run M+ en cours.")
        end
        local nruns = 0
        for _ in pairs(KeystoneTrustDB.runs or {}) do nruns = nruns + 1 end
        print("|cff00ccff[KT]|r " .. nruns .. " run(s) enregistrés localement.")

    elseif cmd == "clear" then
        KeystoneTrustDB.runs = {}
        print("|cff00ccff[KT]|r Logs de runs effacés.")

    elseif cmd == "clearcache" then
        KT_BENCHMARKS = {}
        print("|cff00ccff[KT]|r Cache KT_BENCHMARKS effacé — relancez une sync depuis le client KT.")

    elseif cmd == "bench" then
        local db = GetKTCache(KT.myKey or "")
        local dungeons = db and db.roles and (db.roles.DPS or db.roles.HEALER or db.roles.TANK)
        if not db or not dungeons or #dungeons == 0 then
            print("|cff00ccff[KT]|r Aucun benchmark disponible.")
            print("|cff00ccff[KT]|r Lance une analyse depuis le client KT, puis /reload.")
            return
        end
        local char = db.character or {}
        print("|cff00ccff[KT]|r ── Benchmarks" ..
              (char.name and (" — " .. char.name .. "-" .. (char.realm or "")) or "") .. " ──")
        for _, d in ipairs(dungeons) do
            local rank     = d.rank
            local rankStr  = rank and ("|cff" .. (TOOLTIP_RANK_COLOR[rank] or "9CA3AF") ..
                             "[" .. (TOOLTIP_RANK_LABEL[rank] or rank) .. "]|r ") or ""
            local pctStr   = d.topPct and ("top " .. tostring(d.topPct) .. "%") or "—"
            local isH      = db.roles.HEALER == dungeons
            local perfStr  = isH and ("HPS " .. FmtK(d.hps)) or ("DPS " .. FmtK(d.dps))
            local kickStr  = d.kicks  ~= nil and (tostring(d.kicks)  .. " kick") or "— kick"
            local deathStr = d.deaths ~= nil and (tostring(d.deaths) .. " mort") or "— mort"
            local lvlStr   = d.level and ("+" .. tostring(d.level)) or "—"
            print("|cff00ccff[KT]|r " .. (d.name or "?") .. "  " .. lvlStr ..
                  "  " .. rankStr .. pctStr .. "  " .. perfStr ..
                  "  " .. kickStr .. "  " .. deathStr)
        end

    elseif cmd == "maps" then
        local maps = C_ChallengeMode.GetMapTable and C_ChallengeMode.GetMapTable() or {}
        if #maps == 0 then
            print("|cff00ccff[KT]|r Aucun donjon de saison trouvé (hors d'un donjon M+ ?)")
        else
            print("|cff00ccff[KT]|r Donjons M+ saison en cours :")
            for _, mapID in ipairs(maps) do
                local name = C_ChallengeMode.GetMapUIInfo and C_ChallengeMode.GetMapUIInfo(mapID) or "?"
                print("|cff00ccff[KT]|r   mapID=|cffffcc00" .. mapID .. "|r → " .. tostring(name))
            end
        end

    elseif cmd == "debugtt" then
        KT._debugtt = not KT._debugtt
        print("|cff00ccff[KT]|r Debug tooltip " .. (KT._debugtt and "|cff00ff00ON|r — survole un ami/applicant" or "|cffff4444OFF|r"))

    else
        print("|cff00ccff[KT]|r /kt status | /kt bench | /kt clear | /kt clearcache | /kt maps | /kt debugtt")
    end
end
