-- ============================================================
-- Asylum Compagnon — SimC.lua
-- Génère un profil SimulationCraft aligné sur l'addon officiel
-- (github.com/simulationcraft/simc-addon, licence Unlicense) :
-- mêmes champs, même ordre, même checksum adler32 — accepté par
-- https://www.raidbots.com/simbot/topgear sans avertissement.
-- ============================================================

local ADDON_NAME, NS = ...

-- Slots d'équipement, dans l'ordre attendu par SimC
local SLOT_MAP = {
    { inv = 1,  simc = "head" },
    { inv = 2,  simc = "neck" },
    { inv = 3,  simc = "shoulder" },
    { inv = 15, simc = "back" },
    { inv = 5,  simc = "chest" },
    { inv = 9,  simc = "wrist" },
    { inv = 10, simc = "hands" },
    { inv = 6,  simc = "waist" },
    { inv = 7,  simc = "legs" },
    { inv = 8,  simc = "feet" },
    { inv = 11, simc = "finger1" },
    { inv = 12, simc = "finger2" },
    { inv = 13, simc = "trinket1" },
    { inv = 14, simc = "trinket2" },
    { inv = 16, simc = "main_hand" },
    { inv = 17, simc = "off_hand" },
}

-- Ordre d'affichage des slots pour la section « Gear from Bags »
local SIMC_SLOT_ORDER = {}
for i, s in ipairs(SLOT_MAP) do SIMC_SLOT_ORDER[s.simc] = i end

-- Emplacement d'équipement → slot SimC (pour le stuff des sacs)
local EQUIPLOC_TO_SIMC = {
    INVTYPE_HEAD = "head", INVTYPE_NECK = "neck", INVTYPE_SHOULDER = "shoulder",
    INVTYPE_CLOAK = "back", INVTYPE_CHEST = "chest", INVTYPE_ROBE = "chest",
    INVTYPE_WRIST = "wrist", INVTYPE_HAND = "hands", INVTYPE_WAIST = "waist",
    INVTYPE_LEGS = "legs", INVTYPE_FEET = "feet",
    INVTYPE_FINGER = "finger1", INVTYPE_TRINKET = "trinket1",
    INVTYPE_WEAPON = "main_hand", INVTYPE_2HWEAPON = "main_hand",
    INVTYPE_WEAPONMAINHAND = "main_hand",
    INVTYPE_WEAPONOFFHAND = "off_hand", INVTYPE_HOLDABLE = "off_hand",
    INVTYPE_SHIELD = "off_hand",
    INVTYPE_RANGED = "main_hand", INVTYPE_RANGEDRIGHT = "main_hand",
}

-- specID → nom de spé au format SimC
NS.SPEC_TOKENS = {
    [62]  = "arcane",        [63]  = "fire",         [64]  = "frost",
    [65]  = "holy",          [66]  = "protection",   [70]  = "retribution",
    [71]  = "arms",          [72]  = "fury",         [73]  = "protection",
    [102] = "balance",       [103] = "feral",        [104] = "guardian",     [105] = "restoration",
    [250] = "blood",         [251] = "frost",        [252] = "unholy",
    [253] = "beast_mastery", [254] = "marksmanship", [255] = "survival",
    [256] = "discipline",    [257] = "holy",         [258] = "shadow",
    [259] = "assassination", [260] = "outlaw",       [261] = "subtlety",
    [262] = "elemental",     [263] = "enhancement",  [264] = "restoration",
    [265] = "affliction",    [266] = "demonology",   [267] = "destruction",
    [268] = "brewmaster",    [269] = "windwalker",   [270] = "mistweaver",
    [577] = "havoc",         [581] = "vengeance",
    [1467] = "devastation",  [1468] = "preservation", [1473] = "augmentation",
}

local ROLE_MAP = { DAMAGER = "attack", TANK = "tank", HEALER = "heal" }

NS.RACE_OVERRIDES = { Scourge = "undead" }

-- Types de « modifiers » dans les liens d'objets (mêmes constantes
-- que l'addon officiel ; Enum en jeu si dispo, repli statique)
local IM = Enum and Enum.ItemModification or {}
local MOD_DROP_LEVEL     = 9
local MOD_CONTENT_TUNING = IM.ContentTuningID or 28
local MOD_CRAFT_STAT_1   = IM.ChangeModifiedCraftingStat1 or 29
local MOD_CRAFT_STAT_2   = IM.ChangeModifiedCraftingStat2 or 30
local MOD_REDIRECTED     = 64 -- 12.1 : catalyst base stats
local MOD_CRAFT_QUALITY  = IM.CraftingQualityID or 38

-- Offsets dans la chaîne d'objet (après « item: »)
local OFF_ITEM, OFF_ENCHANT, OFF_GEM1, OFF_GEM4, OFF_SUFFIX, OFF_BONUS = 1, 2, 3, 6, 7, 13

local function SnakeCase(token)
    return (token:gsub("(%l)(%u)", "%1_%2"):lower())
end

-- Nom d'objet sans icônes de qualité (|A...|a), textures (|T...|t) ni pipes
local function CleanName(link)
    local n = link:match("%[(.-)%]") or ""
    n = n:gsub("|A.-|a", ""):gsub("|T.-|t", ""):gsub("|", "")
    n = n:gsub("%s+$", ""):gsub("^%s+", "")
    return n
end

local function ItemLevelOf(link)
    local fn = (C_Item and C_Item.GetDetailedItemLevelInfo) or GetDetailedItemLevelInfo
    if fn then
        local ok, ilvl = pcall(fn, link)
        if ok and ilvl then return ilvl end
    end
end

-- Checksum adler32 (RFC 1950), identique à l'addon officiel :
-- c'est elle que Raidbots vérifie pour détecter une entrée modifiée.
local function Adler32(s)
    local prime = 65521
    local s1, s2 = 1, 0
    for i = 1, #s do
        s1 = s1 + s:byte(i)
        s2 = s2 + s1
    end
    s1 = s1 % prime
    s2 = s2 % prime
    return bit.lshift(s2, 16) + s1
end

-- Décortique un lien d'objet (champs vides → 0, tout en nombres)
local function ParseLink(link)
    local inner = link:match("item:([%-%d:]*)")
    if not inner then return end
    local f = {}
    for _, v in ipairs({ strsplit(":", inner) }) do
        f[#f + 1] = tonumber(v) or 0
    end
    if not f[OFF_ITEM] or f[OFF_ITEM] == 0 then return end

    local p = { id = f[OFF_ITEM], gems = {}, bonuses = {}, modsList = {}, craftedStats = {} }

    if f[OFF_ENCHANT] and f[OFF_ENCHANT] > 0 then p.enchant = f[OFF_ENCHANT] end

    for i = OFF_GEM1, OFF_GEM4 do
        if f[i] and f[i] > 0 then p.gems[#p.gems + 1] = f[i] end
    end

    if f[OFF_SUFFIX] and f[OFF_SUFFIX] ~= 0 then p.suffix = f[OFF_SUFFIX] end

    local numBonus = f[OFF_BONUS] or 0
    for i = 1, numBonus do
        local b = f[OFF_BONUS + i]
        if b and b ~= 0 then p.bonuses[#p.bonuses + 1] = b end
    end

    -- modifiers : paires (type, valeur) après les bonus
    local linkOffset = OFF_BONUS + numBonus + 1
    local numPairs = f[linkOffset] or 0
    for index = 1, numPairs do
        local off = 1 + linkOffset + 2 * (index - 1)
        local mType, mValue = f[off], f[off + 1]
        if mType == MOD_CRAFT_STAT_1 or mType == MOD_CRAFT_STAT_2 then
            p.craftedStats[#p.craftedStats + 1] = mValue
        elseif mType then
            p.modsList[#p.modsList + 1] = { t = mType, v = mValue }
        end
    end

    -- bonus de gemmes (liste après les paires de modifiers)
    local gemBonusOffset = linkOffset + 2 * numPairs + 2
    local numGemBonuses = f[gemBonusOffset] or 0
    if numGemBonuses > 0 then
        p.gemBonuses = {}
        for index = 1, numGemBonuses do
            p.gemBonuses[#p.gemBonuses + 1] = f[gemBonusOffset + index]
        end
    end

    -- qualité d'artisanat : l'API officielle donne le rang (1-5) ;
    -- repli : valeur brute du modifier 38 (décalée de +3)
    if C_TradeSkillUI and C_TradeSkillUI.GetItemCraftedQualityByItemInfo then
        local ok, q = pcall(C_TradeSkillUI.GetItemCraftedQualityByItemInfo, link)
        if ok and q then p.craftQuality = q end
    end
    if not p.craftQuality then
        for index = 1, numPairs do
            local off = 1 + linkOffset + 2 * (index - 1)
            if f[off] == MOD_CRAFT_QUALITY and f[off + 1] then
                local raw = f[off + 1]
                p.craftQuality = (raw > 3) and (raw - 3) or raw
            end
        end
    end

    return p
end

-- Construit la ligne SimC (ordre identique à l'addon officiel)
local function BuildLine(slot, p)
    local parts = { slot .. "=,id=" .. p.id }
    if p.enchant then parts[#parts + 1] = "enchant_id=" .. p.enchant end
    if #p.gems > 0 then parts[#parts + 1] = "gem_id=" .. table.concat(p.gems, "/") end
    if p.suffix then parts[#parts + 1] = "suffix=" .. p.suffix end
    if #p.bonuses > 0 then parts[#parts + 1] = "bonus_id=" .. table.concat(p.bonuses, "/") end
    for _, m in ipairs(p.modsList) do
        if m.t == MOD_DROP_LEVEL then
            parts[#parts + 1] = "drop_level=" .. m.v
        elseif m.t == MOD_CONTENT_TUNING then
            parts[#parts + 1] = "content_tuning=" .. m.v
        elseif m.t == MOD_REDIRECTED then
            parts[#parts + 1] = "redirected_base_stats=" .. m.v
        end
    end
    if #p.craftedStats > 0 then
        parts[#parts + 1] = "crafted_stats=" .. table.concat(p.craftedStats, "/")
    end
    if p.gemBonuses then
        parts[#parts + 1] = "gem_bonus_id=" .. table.concat(p.gemBonuses, "/")
    end
    if p.craftQuality then
        parts[#parts + 1] = "crafting_quality=" .. p.craftQuality
    end
    return table.concat(parts, ",")
end

-- Méta du personnage (classe, niveau, spé, rôle, ilvl)
function NS.GetMyMeta()
    local _, classToken = UnitClass("player")

    local specID, role
    local getSpec = (C_SpecializationInfo and C_SpecializationInfo.GetSpecialization) or GetSpecialization
    local getSpecInfo = (C_SpecializationInfo and C_SpecializationInfo.GetSpecializationInfo) or GetSpecializationInfo
    if getSpec and getSpecInfo then
        local idx = getSpec()
        if idx then
            local id, _, _, _, r = getSpecInfo(idx)
            specID, role = id, r
        end
    end

    local ilvl = 0
    if GetAverageItemLevel then
        local _, equipped = GetAverageItemLevel()
        ilvl = math.floor((equipped or 0) + 0.5)
    end

    return {
        class  = classToken or "?",
        level  = UnitLevel("player") or 0,
        specID = specID or 0,
        role   = role,
        ilvl   = ilvl,
    }
end

-- Nombre d'un item possédé (sacs + banque)
function NS.CountItem(itemID)
    if C_Item and C_Item.GetItemCount then
        local ok, n = pcall(C_Item.GetItemCount, itemID, true, false, true)
        if ok and n then return n end
    end
    if GetItemCount then
        local ok, n = pcall(GetItemCount, itemID, true)
        if ok and n then return n end
    end
    return 0
end

-- Total d'une entrée suivie (somme de tous ses itemID / paliers de qualité)
function NS.CountTracked(entry)
    local total = 0
    for _, id in ipairs(entry.ids or {}) do
        total = total + NS.CountItem(id)
    end
    return total
end

-- Recalcul LÉGER des quantités de consommables suivis (juste des
-- GetItemCount, SANS générer le profil SimC). Appelable souvent sans coût
-- → utilisé par l'onglet Inventaire. Met à jour NS.db.myProfile.items.
function NS.RefreshTrackedItems()
    if not NS.db then return end
    NS.db.myProfile = NS.db.myProfile or {}
    local prev = NS.db.myProfile.items
    local items = {}
    for _, t in ipairs(NS.TRACKED_ITEMS or {}) do
        local pid = t.ids and t.ids[1]
        if pid then
            local c = NS.CountTracked(t)
            if c > 0 and NS.chardb then NS.chardb.itemsSeen[pid] = true end
            if c > 0 or (NS.chardb and NS.chardb.itemsSeen[pid]) then items[pid] = c end
            if c == 0 and prev and (prev[pid] or 0) > 0 and NS.NotifyDepleted then NS.NotifyDepleted(pid) end
            local low = NS.LOW_STOCK or 5
            if c > 0 and c <= low and prev and (prev[pid] or 0) > low and NS.NotifyLow then NS.NotifyLow(pid, c) end
        end
    end
    NS.db.myProfile.items = items
    return items
end

-- Monnaies du catalyseur + hauts-faits d'amélioration (12.0.7, listes
-- observées depuis l'addon officiel — utilisées par le Droptimizer)
local CATALYST_CURRENCIES = { 3269, 3378, 2813, 3116 }
local UPGRADE_ACHIEVEMENTS = {
    19326, 19397, 19577, 19578, 19579, 40107, 40115, 40118,
    40942, 40943, 40944, 40945, 41886, 41887, 41888, 41892,
    42767, 42768, 61809,
}

-- Génère le profil SimC complet
function NS.GenerateSimC()
    local name = UnitName("player") or "Perso"
    local meta = NS.GetMyMeta()
    local _, raceToken = UnitRace("player")
    local specToken = NS.SPEC_TOKENS[meta.specID]

    local activeCfg = C_ClassTalents and C_ClassTalents.GetActiveConfigID
        and C_ClassTalents.GetActiveConfigID()

    local talents
    if activeCfg and C_Traits and C_Traits.GenerateImportString then
        local ok, s = pcall(C_Traits.GenerateImportString, activeCfg)
        if ok and type(s) == "string" and s ~= "" then talents = s end
    end

    local region = ({ "us", "kr", "eu", "tw", "cn" })[(GetCurrentRegion and GetCurrentRegion()) or 3] or "eu"
    local serverRaw = (GetRealmName and GetRealmName()) or ""
    local server = serverRaw:lower():gsub("[%s'%-]", "")

    local L = {}
    local function add(s) L[#L + 1] = s end

    -- entête (même forme que l'addon officiel)
    local specHeader = specToken and (specToken:gsub("^%l", string.upper):gsub("_", " ")) or "?"
    add(("# %s - %s - %s - %s/%s"):format(name, specHeader, date("%Y-%m-%d %H:%M"), region:upper(), serverRaw))
    add("# SimC Addon 12.1.0") -- ligne de compatibilité : Raidbots identifie l'origine par cet en-tête
    add(("# (generated by Asylum Compagnon v%s)"):format(NS.VERSION or "?"))
    if GetBuildInfo then
        local v, build, _, toc = GetBuildInfo()
        add(("# WoW %s.%s, TOC %s"):format(v or "?", build or "?", toc or "?"))
    end
    add("# Requires SimulationCraft 1000-01 or newer")
    add("")
    add(("%s=\"%s\""):format((meta.class or "?"):lower(), name))
    add("level=" .. meta.level)
    add("race=" .. (NS.RACE_OVERRIDES[raceToken] or SnakeCase(raceToken or "Human")))
    add("region=" .. region)
    if server ~= "" then add("server=" .. server) end
    if meta.role and ROLE_MAP[meta.role] then add("role=" .. ROLE_MAP[meta.role]) end
    if specToken then add("spec=" .. specToken) end
    if GetLootSpecialization then
        local ls = GetLootSpecialization()
        local lsToken = (ls and ls ~= 0) and NS.SPEC_TOKENS[ls] or specToken
        if lsToken then add("# loot_spec=" .. lsToken) end
    end
    if talents then add("talents=" .. talents) end

    -- loadouts sauvegardés, en commentaires (comme l'addon officiel)
    if activeCfg and meta.specID > 0 and C_ClassTalents.GetConfigIDsBySpecID then
        local ok, configs = pcall(C_ClassTalents.GetConfigIDsBySpecID, meta.specID)
        if ok and type(configs) == "table" then
            for _, cid in ipairs(configs) do
                if cid ~= activeCfg then
                    local info = C_Traits.GetConfigInfo and C_Traits.GetConfigInfo(cid)
                    local lname = (info and info.name and info.name:gsub("|", "")) or "?"
                    local ok2, s2 = pcall(C_Traits.GenerateImportString, cid)
                    if ok2 and type(s2) == "string" and s2 ~= "" then
                        add("# Saved Loadout: " .. lname)
                        add("# talents=" .. s2)
                    end
                end
            end
        end
    end
    add("")

    -- équipement porté
    for _, s in ipairs(SLOT_MAP) do
        local link = GetInventoryItemLink("player", s.inv)
        if link then
            local p = ParseLink(link)
            if p then
                local ilvl = ItemLevelOf(link)
                add(("# %s%s"):format(CleanName(link), ilvl and (" (" .. ilvl .. ")") or ""))
                add(BuildLine(s.simc, p))
            end
        end
    end

    -- stuff équipable dans les sacs (trié par slot, comme l'officiel)
    local getNumSlots = C_Container and C_Container.GetContainerNumSlots
    local getLink = C_Container and C_Container.GetContainerItemLink
    local getInstant = (C_Item and C_Item.GetItemInfoInstant) or GetItemInfoInstant
    if getNumSlots and getLink and getInstant then
        local bagItems = {}
        for bag = 0, 4 do
            for slot = 1, (getNumSlots(bag) or 0) do
                local link = getLink(bag, slot)
                if link and link:find("item:") then
                    local ok, _, _, _, equipLoc = pcall(getInstant, link)
                    local simcSlot = ok and equipLoc and EQUIPLOC_TO_SIMC[equipLoc]
                    if simcSlot then
                        local p = ParseLink(link)
                        if p then
                            local ilvl = ItemLevelOf(link)
                            bagItems[#bagItems + 1] = {
                                order = SIMC_SLOT_ORDER[simcSlot] or 99,
                                name = ("%s%s"):format(CleanName(link), ilvl and (" (" .. ilvl .. ")") or ""),
                                line = BuildLine(simcSlot, p),
                            }
                        end
                    end
                end
            end
        end
        table.sort(bagItems, function(a, b) return a.order < b.order end)
        if #bagItems > 0 then
            add("")
            add("### Gear from Bags")
            for _, it in ipairs(bagItems) do
                add("#")
                add("# " .. it.name)
                add("# " .. it.line)
            end
        end
    end

    -- infos additionnelles (utilisées par le Droptimizer de Raidbots)
    add("")
    add("### Additional Character Info")
    if C_CurrencyInfo and C_CurrencyInfo.GetCurrencyInfo then
        local cur = {}
        for _, cid in ipairs(CATALYST_CURRENCIES) do
            local ok, info = pcall(C_CurrencyInfo.GetCurrencyInfo, cid)
            if ok and info and info.quantity then
                cur[#cur + 1] = cid .. ":" .. info.quantity
            end
        end
        add("#")
        add("# catalyst_currencies=" .. table.concat(cur, "/"))
    end
    if C_ItemUpgrade and C_ItemUpgrade.GetHighWatermarkForSlot then
        local marks = {}
        for slot = 0, 16 do
            local ok, charMark, acctMark = pcall(C_ItemUpgrade.GetHighWatermarkForSlot, slot)
            if ok and (charMark or acctMark) then
                marks[#marks + 1] = slot .. ":" .. (charMark or 0) .. ":" .. (acctMark or 0)
            end
        end
        if #marks > 0 then
            add("#")
            add("# slot_high_watermarks=" .. table.concat(marks, "/"))
        end
    end
    if GetAchievementInfo then
        local achs = {}
        for _, aid in ipairs(UPGRADE_ACHIEVEMENTS) do
            local ok, _, _, _, complete = pcall(GetAchievementInfo, aid)
            if ok and complete then achs[#achs + 1] = aid end
        end
        add("#")
        add("# upgrade_achievements=" .. table.concat(achs, "/"))
    end
    add("")

    -- checksum adler32 (vérifiée par Raidbots), calculée sur tout
    -- le texte qui précède, saut de ligne final inclus
    local body = table.concat(L, "\n") .. "\n"
    return body .. "# Checksum: " .. string.format("%x", Adler32(body))
end

-- Corps « stable » d'un profil SimC : on retire la 1re ligne d'entête (elle
-- contient la date → change à chaque génération) et la ligne de checksum
-- (calculée sur un texte incluant cette date). Sert à savoir si le profil
-- a RÉELLEMENT changé entre deux régénérations.
local function ProfileBody(s)
    s = (s or ""):gsub("^#[^\n]*\n", "", 1)
    s = s:gsub("# Checksum: %x+%s*$", "")
    return s
end

-- Deux tables d'items identiques ? (mêmes clés, mêmes quantités)
local function SameItems(a, b)
    if not a or not b then return false end
    for k, v in pairs(a) do if b[k] ~= v then return false end end
    for k in pairs(b) do if a[k] == nil then return false end end
    return true
end

-- Régénère et stocke le profil (+ or et items suivis) dans les SavedVariables.
-- Anti-spam réseau : le hello n'est diffusé QUE si quelque chose d'utile a
-- changé (méta, flags, items suivis ou corps du profil). Avant, en farm,
-- chaque tick or/sacs (12 s) partait en guilde + groupe pour rien. Le ts du
-- profil n'avance que si le code SimC a réellement changé, sinon les amis
-- verraient « plus récent dispo » à tort.
function NS.UpdateMyProfile(broadcast)
    if not NS.db then return end
    local ok, code = pcall(NS.GenerateSimC)
    if not ok or type(code) ~= "string" then return end

    local meta = NS.GetMyMeta()
    local old = NS.db.myProfile
    -- quantités des consommables suivis (+ alertes stock bas / épuisé) —
    -- même logique que l'onglet Inventaire, une seule implémentation
    local items = NS.RefreshTrackedItems() or {}
    local flags = (NS.Buffs and NS.Buffs.SelfFlags and NS.Buffs.SelfFlags()) or ""

    local sameCode = old and old.simc and ProfileBody(old.simc) == ProfileBody(code) or false
    local sameMeta = old and old.class == meta.class and old.level == meta.level
        and old.specID == meta.specID and old.ilvl == meta.ilvl
        and (old.flags or "") == flags or false
    local sameItems = old and SameItems(old.items, items) or false

    NS.db.myProfile = {
        simc   = code,
        ts     = (sameCode and old.ts) or (GetServerTime and GetServerTime()) or time(),
        class  = meta.class,
        level  = meta.level,
        specID = meta.specID,
        ilvl   = meta.ilvl,
        gold   = math.floor(((GetMoney and GetMoney()) or 0) / 10000),
        items  = items,
        flags  = flags,
    }

    -- l'or seul ne déclenche pas de hello (il n'est plus affiché nulle part
    -- depuis la 0.11) : il voyagera avec le prochain heartbeat (≤ 60 s)
    if broadcast and NS.Sync and NS.Sync.available
        and not (sameMeta and sameItems and sameCode) then
        NS.Sync.BroadcastHello(false)
    end
    if NS.RefreshOptimUI then NS.RefreshOptimUI("self") end
end
