-- ============================================================
-- Asylum Compagnon — Vault.lua
-- Clés Mythique+ de la semaine + Coffre convoité (Great Vault).
-- Chaque client lit sa clé (C_MythicPlus) et ses 9 cases de coffre
-- (C_WeeklyRewards.GetActivities) et les diffuse (« V ») ; les autres
-- les mettent en cache dans peers[key].keystone / peers[key].vault.
-- La page « Clés & Coffre » liste tout le monde ; le cadre des 9 cases
-- s'affiche au survol du nom.
-- ============================================================

local ADDON_NAME, NS = ...

local Vault = {}
NS.Vault = Vault

-- Libellés des pistes du coffre (Raid / Donjons / Monde / PvP)
local TYPE_LABEL = { [1] = "Donjons", [2] = "PvP", [3] = "Raid", [6] = "Monde", [7] = "Gouffres" }
do
    local E = Enum and Enum.WeeklyRewardChestThresholdType
    if E then
        if E.Raid then TYPE_LABEL[E.Raid] = "Raid" end
        if E.Activities then TYPE_LABEL[E.Activities] = "Donjons" end
        if E.MythicPlus then TYPE_LABEL[E.MythicPlus] = "Donjons" end
        if E.World then TYPE_LABEL[E.World] = "Monde" end
        if E.RankedPvP then TYPE_LABEL[E.RankedPvP] = "PvP" end
    end
end
NS.VAULT_TYPE_LABEL = TYPE_LABEL

-- ---------- Lecture locale ----------
-- Repli : lit la clé directement dans les SACS via le lien de l'objet
-- (« |Hkeystone:itemID:mapID:niveau:affixes… »). L'API C_MythicPlus renvoie
-- parfois 0 alors que la clé est bien là (données pas encore chargées côté
-- client, notamment à la connexion) → sans ce repli, « aucune clé » à tort.
local function ReadKeystoneFromBags()
    local getSlots = C_Container and C_Container.GetContainerNumSlots
    local getLink  = C_Container and C_Container.GetContainerItemLink
    if not (getSlots and getLink) then return 0, 0 end
    for bag = 0, 5 do
        for slot = 1, (getSlots(bag) or 0) do
            local ok, link = pcall(getLink, bag, slot)
            if ok and type(link) == "string" then
                local mapID, level = link:match("Hkeystone:%d+:(%d+):(%d+)")
                if mapID then return tonumber(mapID) or 0, tonumber(level) or 0 end
            end
        end
    end
    return 0, 0
end

local function ReadKeystone()
    local mapID, level = 0, 0
    if C_MythicPlus then
        -- le nom de l'API varie selon les versions : le nom RETAIL correct est
        -- GetOwnedKeystoneChallengeMapID (GetOwnedKeystoneMapID n'existe pas
        -- partout → il renvoyait nil et on affichait « aucune clé » à tort)
        for _, fn in ipairs({ "GetOwnedKeystoneChallengeMapID", "GetOwnedKeystoneMapID" }) do
            local f = C_MythicPlus[fn]
            if f and mapID == 0 then
                local ok, v = pcall(f)
                if ok and type(v) == "number" and v > 0 then mapID = v end
            end
        end
        if C_MythicPlus.GetOwnedKeystoneLevel then
            local ok, v = pcall(C_MythicPlus.GetOwnedKeystoneLevel)
            if ok and type(v) == "number" and v > 0 then level = v end
        end
    end
    if mapID == 0 or level == 0 then
        local m2, l2 = ReadKeystoneFromBags()
        if m2 ~= 0 and l2 ~= 0 then mapID, level = m2, l2 end
    end
    return mapID, level
end

-- « /asylum keydebug » : diagnostic de la détection de clé — affiche ce que
-- répond chaque API et les liens « keystone » trouvés dans les sacs (avec
-- leurs codes bruts). À coller au dev si la clé n'est toujours pas détectée.
function Vault.DebugKey()
    if not NS.Msg then return end
    NS.Msg("diagnostic clé M+ :")
    if C_MythicPlus then
        for _, fn in ipairs({ "GetOwnedKeystoneChallengeMapID",
            "GetOwnedKeystoneMapID", "GetOwnedKeystoneLevel" }) do
            local f = C_MythicPlus[fn]
            if f then
                local ok, v = pcall(f)
                print(("   C_MythicPlus.%s → %s"):format(fn,
                    ok and tostring(v) or ("ERREUR " .. tostring(v))))
            else
                print(("   C_MythicPlus.%s → absente"):format(fn))
            end
        end
    else
        print("   C_MythicPlus indisponible")
    end
    local getSlots = C_Container and C_Container.GetContainerNumSlots
    local getLink  = C_Container and C_Container.GetContainerItemLink
    local found = 0
    if getSlots and getLink then
        for bag = 0, 5 do
            for slot = 1, (getSlots(bag) or 0) do
                local ok, link = pcall(getLink, bag, slot)
                if ok and type(link) == "string" and link:lower():find("keystone", 1, true) then
                    found = found + 1
                    -- || : affiche les codes du lien en brut (structure lisible)
                    print(("   sac %d, case %d : %s"):format(bag, slot, link:gsub("|", "||")))
                end
            end
        end
    end
    if found == 0 then print("   aucun lien « keystone » dans les sacs") end
    local m, l = ReadKeystone()
    print(("   → lecture finale : mapID=%d, niveau=%d"):format(m, l))
end

local function ReadVault()
    local out = {}
    if C_WeeklyRewards and C_WeeklyRewards.GetActivities then
        local ok, acts = pcall(C_WeeklyRewards.GetActivities)
        if ok and type(acts) == "table" then
            for _, a in ipairs(acts) do
                out[#out + 1] = {
                    type = a.type or 0, index = a.index or 0,
                    progress = a.progress or 0, threshold = a.threshold or 0,
                    level = a.level or 0,
                }
            end
        end
    end
    return out
end

function Vault.KeystoneName(mapID)
    if not mapID or mapID == 0 then return nil end
    if C_ChallengeMode and C_ChallengeMode.GetMapUIInfo then
        local ok, name = pcall(C_ChallengeMode.GetMapUIInfo, mapID)
        if ok and name and name ~= "" then return name end
    end
    return nil
end

-- Texte de clé robuste : « <Donjon> +<niveau> ». Si le NOM du donjon n'est pas
-- résolu par le client (ex. une clé Nexus dont les données M+ ne sont pas encore
-- chargées) MAIS que le niveau est connu, on affiche quand même « Donjon +X »
-- au lieu de « aucune clé » (le membre concerné n'était plus annoncé du tout).
function Vault.KeyText(mapID, level)
    level = tonumber(level) or 0
    if level <= 0 then return nil end
    return (Vault.KeystoneName(mapID) or NS.L("Donjon")) .. " +" .. level
end

-- ---------- Sérialisation / diffusion ----------
function Vault.SerializeMine()
    local mapID, level = ReadKeystone()
    local acts = ReadVault()
    local parts = {}
    for _, a in ipairs(acts) do
        parts[#parts + 1] = ("%d,%d,%d,%d,%d"):format(a.type, a.index, a.progress, a.threshold, a.level)
    end
    return ("%d:%d\t%s"):format(mapID, level, table.concat(parts, ";"))
end

local entered = false
local lastKeySig -- « mapID:niveau » de la dernière clé annoncée (anti-spam)
function Vault.BroadcastMine()
    if not (NS.Sync and NS.Sync.available and NS.Sync.BroadcastVault) then return end
    local mapID, level = ReadKeystone()
    lastKeySig = mapID .. ":" .. level
    NS.Sync.BroadcastVault(Vault.SerializeMine())
end

-- La clé dans les sacs a-t-elle changé depuis la dernière annonce ?
-- (utilisé sur BAG_UPDATE_DELAYED : nouvelle clé lootée / clé retirée)
function Vault.KeyChangedSinceLastSend()
    local mapID, level = ReadKeystone()
    return (mapID .. ":" .. level) ~= lastKeySig
end

-- Réception d'un « V »
function Vault.OnPeerVault(sender, msg)
    local body = msg:match("^V\t(.*)$")
    if not body or not NS.db then return end
    local keyPart, vaultPart = body:match("^([^\t]*)\t(.*)$")
    if not keyPart then keyPart = body; vaultPart = "" end
    NS.db.peers = NS.db.peers or {}
    local p = NS.db.peers[sender]
    if not p then p = {}; NS.db.peers[sender] = p end
    local mapID, level = keyPart:match("^(%-?%d+):(%-?%d+)$")
    p.keystone = { mapID = tonumber(mapID) or 0, level = tonumber(level) or 0 }
    local vault = {}
    for chunk in (vaultPart or ""):gmatch("[^;]+") do
        local t, i, pr, th, lv = chunk:match("^(%-?%d+),(%-?%d+),(%-?%d+),(%-?%d+),(%-?%d+)$")
        if t then
            vault[#vault + 1] = {
                type = tonumber(t), index = tonumber(i),
                progress = tonumber(pr), threshold = tonumber(th), level = tonumber(lv),
            }
        end
    end
    p.vault = vault
    p.lastSeen = (GetServerTime and GetServerTime()) or time()
    if NS.RefreshVaultUI then NS.RefreshVaultUI() end
end

-- Écho aux hellos (anti-rafale) — comme le module butin
local helloEcho
function Vault.OnPeerHello()
    if helloEcho then return end
    helloEcho = C_Timer.NewTimer(6, function()
        helloEcho = nil
        Vault.BroadcastMine()
    end)
end

-- ---------- Rapport pour l'UI ----------
-- Texte de MA clé de la semaine (accueil) : « Donjon +12 », ou nil sans clé
function Vault.MyKeyText()
    local mapID, level = ReadKeystone()
    return Vault.KeyText(mapID, level)
end

-- « /asylum keys » : liste dans le chat les clés M+ du GROUPE ACTUEL
-- uniquement (toi + les membres présents qui ont l'addon) — jamais les
-- amis hors groupe. Solo : juste ta clé.
function Vault.PrintGroupKeys()
    if not NS.Msg then return end
    local lines = {}
    local function keyTextOf(mapID, level)
        return Vault.KeyText(mapID, level) or NS.L("aucune clé")
    end
    local mapID, level = ReadKeystone()
    local _, myClass = UnitClass("player")
    lines[#lines + 1] = { name = UnitName("player") or "?", class = myClass,
        me = true, txt = keyTextOf(mapID, level) }
    local seen = {}
    local function addPeer(k)
        local p = NS.db and NS.db.peers and NS.db.peers[k]
        if not p or seen[k] then return end
        seen[k] = true
        lines[#lines + 1] = { name = k:match("^(.-)%-") or k, class = p.class,
            txt = p.keystone and keyTextOf(p.keystone.mapID, p.keystone.level)
                or NS.L("aucune clé") }
    end
    if IsInGroup and IsInGroup() then
        local prefix, n = "party", 4
        if IsInRaid and IsInRaid() then prefix, n = "raid", 40 end
        for i = 1, n do
            local u = prefix .. i
            if not UnitExists or UnitExists(u) then
                local k = NS.UnitKey(u)
                if k then addPeer(k) end
            end
        end
    end
    NS.Msg(NS.L("Clés M+ :"))
    for _, e in ipairs(lines) do
        local c = e.class and RAID_CLASS_COLORS and RAID_CLASS_COLORS[e.class]
        print(("   |c%s%s|r%s : %s"):format((c and c.colorStr) or "ffffffff",
            e.name, "", e.txt))
    end
end

-- ---------- Niveaux d'objet du reward (Midnight Saison 1) ----------
-- Source : addon GreatVaultKeyInfo / wago.tools (rewardSeasonID 117).
-- On déduit l'ilvl du reward à partir de (type, niveau) — déjà partagés,
-- donc rien à changer dans la synchro. À MAJ si la saison change.
local RWD_DUNGEON = { [2]=259,[3]=259,[4]=263,[5]=263,[6]=266,[7]=269,[8]=269,[9]=269,[10]=272 }
local RWD_DUNGEON_M0 = 256
local RWD_WORLD = { [1]=233,[2]=237,[3]=240,[4]=243,[5]=246,[6]=253,[7]=256,[8]=259 }
local RWD_RAID  = { [17]=233,[14]=246,[15]=259,[16]=272 } -- LFR / Normal / Héroïque / Mythique
local RAID_ORDER = { 17, 14, 15, 16 }

local T_DUNGEON, T_WORLD, T_RAID = 1, 6, 3
do
    local E = Enum and Enum.WeeklyRewardChestThresholdType
    if E then
        T_DUNGEON = E.Activities or E.MythicPlus or T_DUNGEON
        T_WORLD   = E.World or T_WORLD
        T_RAID    = E.Raid or T_RAID
    end
end

-- ilvl du reward pour (type, niveau), ou nil si inconnu
function Vault.RewardIlvl(typ, level)
    level = level or 0
    if typ == T_RAID then return RWD_RAID[level] end
    if typ == T_WORLD then return RWD_WORLD[math.min(math.max(level, 1), 8)] end
    if typ == T_DUNGEON then
        if level >= 2 then return RWD_DUNGEON[math.min(level, 10)] end
        if level == 1 or level == 0 then return RWD_DUNGEON_M0 end
    end
    return nil
end

-- Prochain palier utile (niveau, ilvl) au-dessus du reward actuel, ou nil si max
function Vault.NextTier(typ, level)
    local cur = Vault.RewardIlvl(typ, level)
    if not cur then return nil end
    if typ == T_DUNGEON then
        for lv = math.max(level or 0, 1) + 1, 10 do
            local il = RWD_DUNGEON[lv]
            if il and il > cur then return lv, il end
        end
    elseif typ == T_WORLD then
        for lv = (level or 0) + 1, 8 do
            local il = RWD_WORLD[lv]
            if il and il > cur then return lv, il end
        end
    elseif typ == T_RAID then
        local seen = false
        for _, d in ipairs(RAID_ORDER) do
            if seen and RWD_RAID[d] and RWD_RAID[d] > cur then return d, RWD_RAID[d] end
            if d == level then seen = true end
        end
    end
    return nil
end

-- Regroupe une liste d'activités par piste (Raid/Donjons/…), 3 cases chacune,
-- avec l'ilvl du reward par case + le palier suivant atteignable par piste.
local function BuildTracks(acts)
    local order, byType = {}, {}
    for _, a in ipairs(acts or {}) do
        if not byType[a.type] then
            byType[a.type] = { label = NS.L(TYPE_LABEL[a.type] or "Piste"), type = a.type, cells = {} }
            order[#order + 1] = a.type
        end
        local filled = (a.progress or 0) >= (a.threshold or 0) and (a.threshold or 0) > 0
        local cell = {
            filled = filled,
            progress = a.progress or 0, threshold = a.threshold or 0, level = a.level or 0,
            ilvl = filled and Vault.RewardIlvl(a.type, a.level) or nil,
        }
        byType[a.type].cells[a.index or (#byType[a.type].cells + 1)] = cell
    end
    local tracks = {}
    for _, t in ipairs(order) do
        local tr = byType[t]
        local best
        for _, c in pairs(tr.cells) do
            if c.filled and c.ilvl and (not best or c.ilvl > best.ilvl) then best = c end
        end
        if best then
            tr.bestIlvl = best.ilvl
            tr.nextLevel, tr.nextIlvl = Vault.NextTier(tr.type, best.level)
        end
        tracks[#tracks + 1] = tr
    end
    return tracks
end

-- Ensemble des membres de MON groupe courant (clés « Nom-Royaume »). En raid :
-- uniquement mon sous-groupe (NS.MyGroupUnits), comme les modules kick/CD.
local function GroupKeySet()
    local set = {}
    for _, u in ipairs((NS.MyGroupUnits and NS.MyGroupUnits()) or {}) do
        local k = NS.UnitKey(u)
        if k then set[k] = true end
    end
    return set
end

-- Ensemble des membres de MA guilde : on stocke le nom complet « Nom-Royaume »
-- ET le nom court (repli robuste aux différences de format de royaume entre le
-- roster de guilde et NS.UnitKey). Sert à classer un pair en ligne hors groupe
-- → section Guilde. Rien n'y figure si je ne suis pas en guilde.
local function GuildKeySet()
    local set = {}
    if not (IsInGuild and IsInGuild()) then return set end
    local n = (GetNumGuildMembers and GetNumGuildMembers()) or 0
    for i = 1, n do
        local full = GetGuildRosterInfo and GetGuildRosterInfo(i)
        if full and full ~= "" then
            set[full] = true
            local bare = full:match("^([^%-]+)")
            if bare then set[bare] = true end
        end
    end
    return set
end

-- Liste { name, class, isMe, section, keyText, tracks } pour la page Clés & Coffre.
-- On n'affiche QUE des joueurs actuellement EN LIGNE, et seulement s'ils sont
-- dans mon GROUPE (section « group », en tête) ou dans ma GUILDE (section
-- « guild », en dessous). Un joueur à la fois dans le groupe ET la guilde est
-- classé dans le groupe ; dès qu'il quitte le groupe il repasse en guilde.
-- Moi : toujours en tête (section groupe).
function Vault.GetReport()
    local mapID, level = ReadKeystone()
    local _, myClass = UnitClass("player")
    -- Classement de MON perso : en groupe → « group » ; solo mais en guilde →
    -- en tête de « guild » ; solo sans guilde → section « solo » (couleur groupe).
    local mySection = (IsInGroup() and "group")
        or (IsInGuild() and "guild")
        or "solo"
    local me = {
        name = UnitName("player") or "?", class = myClass, isMe = true, section = mySection,
        keyText = Vault.KeyText(mapID, level) or NS.L("aucune clé"),
        tracks = BuildTracks(ReadVault()),
    }

    local groupSet, guildSet = GroupKeySet(), GuildKeySet()
    local group, guild = {}, {}
    for k, p in pairs((NS.db and NS.db.peers) or {}) do
        if p.online then                      -- jamais les joueurs déconnectés
            local inGroup = groupSet[k] and true or false
            local bare = k:match("^([^%-]+)")
            local inGuild = (guildSet[k] or (bare and guildSet[bare])) and true or false
            if inGroup or inGuild then
                local mid = p.keystone and p.keystone.mapID or 0
                local lvl = p.keystone and p.keystone.level or 0
                local entry = {
                    name = bare or k, class = p.class, section = inGroup and "group" or "guild",
                    keyText = Vault.KeyText(mid, lvl) or NS.L("aucune clé"),
                    tracks = BuildTracks(p.vault),
                }
                if inGroup then group[#group + 1] = entry else guild[#guild + 1] = entry end
            end
        end
    end
    local byName = function(a, b) return a.name < b.name end
    table.sort(group, byName)
    table.sort(guild, byName)

    -- Moi toujours en TÊTE de ma propre section ; en solo+guilde je m'insère donc
    -- au début de la liste de guilde.
    local list = {}
    if mySection == "guild" then
        list[#list + 1] = me
        for _, e in ipairs(guild) do list[#list + 1] = e end
        for _, e in ipairs(group) do list[#list + 1] = e end
    else -- "group" ou "solo" : moi d'abord, puis groupe, puis guilde
        list[#list + 1] = me
        for _, e in ipairs(group) do list[#list + 1] = e end
        for _, e in ipairs(guild) do list[#list + 1] = e end
    end
    return list
end

-- ---------- Événements ----------
local sendTimer
local function QueueSend()
    if sendTimer then return end
    sendTimer = C_Timer.NewTimer(5, function()
        sendTimer = nil
        Vault.BroadcastMine()
    end)
end

-- Demande au serveur le roster de guilde (nécessaire pour GuildKeySet : sans ça
-- GetNumGuildMembers renvoie 0 tant que le roster n'a pas été chargé).
local function RequestGuild()
    if C_GuildInfo and C_GuildInfo.GuildRoster then pcall(C_GuildInfo.GuildRoster)
    elseif GuildRoster then pcall(GuildRoster) end
end
NS.RequestGuildRoster = RequestGuild

local ev = CreateFrame("Frame")
-- RegisterEvent sous pcall : un nom d'événement inconnu (variantes Midnight)
-- ne doit pas casser le chargement du module.
pcall(ev.RegisterEvent, ev, "PLAYER_ENTERING_WORLD")
pcall(ev.RegisterEvent, ev, "WEEKLY_REWARDS_UPDATE")
pcall(ev.RegisterEvent, ev, "CHALLENGE_MODE_COMPLETED")
pcall(ev.RegisterEvent, ev, "BAG_UPDATE_DELAYED") -- clé lootée / retirée des sacs
pcall(ev.RegisterEvent, ev, "GROUP_ROSTER_UPDATE") -- entrée/sortie de groupe → reclasse
pcall(ev.RegisterEvent, ev, "GUILD_ROSTER_UPDATE") -- roster guilde chargé/mis à jour
ev:SetScript("OnEvent", function(_, event)
    if event == "PLAYER_ENTERING_WORLD" then
        RequestGuild()                    -- charge le roster de guilde tôt
        if not entered then
            entered = true
            -- demande au client de charger les infos M+ (aide l'API à
            -- connaître la clé sans passer par le repli sacs)
            if C_MythicPlus and C_MythicPlus.RequestMapInfo then
                pcall(C_MythicPlus.RequestMapInfo)
            end
            C_Timer.After(8, function() Vault.BroadcastMine() end)
        end
    elseif event == "BAG_UPDATE_DELAYED" then
        -- ne rediffuse QUE si la clé a changé (nouvelle clé, clé consommée) —
        -- pas à chaque loot (cohérent avec l'anti-spam réseau 0.26)
        if entered and Vault.KeyChangedSinceLastSend() then QueueSend() end
    elseif event == "GROUP_ROSTER_UPDATE" or event == "GUILD_ROSTER_UPDATE" then
        -- reclasse groupe/guilde SANS rediffuser (pas de QueueSend ici)
        if NS.RefreshVaultUI then NS.RefreshVaultUI() end
    elseif entered then
        QueueSend()
    end
end)
-- fin de Vault.lua
