-- ============================================================
-- Asylum Compagnon — GearCompassItemParser.lua
-- Extraction défensive du payload item: Retail et normalisation des stats.
-- Le parseur conserve les données brutes et ne dépend d'aucun texte localisé.
-- ============================================================

local _, NS = ...

local Parser = {}
NS.GearCompassItemParser = Parser

local JSON = NS.GearCompassJSON

local ITEM_MOD = Enum and Enum.ItemModification or {}
local MOD_UPGRADE_OBSOLETE = ITEM_MOD.IncrementLevelObsolete or 2
local MOD_CRAFT_STAT_1 = ITEM_MOD.ChangeModifiedCraftingStat1 or 29
local MOD_CRAFT_STAT_2 = ITEM_MOD.ChangeModifiedCraftingStat2 or 30
local MOD_CRAFT_QUALITY = ITEM_MOD.CraftingQualityID or 38

-- Une seule table de correspondance pour toute la fonctionnalité GearCompass.
-- C_Item.GetItemStats renvoie ces clés non localisées (les valeurs globales
-- associées, elles, sont localisées et ne sont donc jamais parsées ici).
Parser.STAT_KEY_MAP = {
    ITEM_MOD_STRENGTH_SHORT = "strength",
    ITEM_MOD_STRENGTH = "strength",
    ITEM_MOD_AGILITY_SHORT = "agility",
    ITEM_MOD_AGILITY = "agility",
    ITEM_MOD_INTELLECT_SHORT = "intellect",
    ITEM_MOD_INTELLECT = "intellect",
    ITEM_MOD_STAMINA_SHORT = "stamina",
    ITEM_MOD_STAMINA = "stamina",
    RESISTANCE0_NAME = "armor",
    ITEM_MOD_ARMOR_SHORT = "armor",
    ITEM_MOD_ARMOR = "armor",
    ITEM_MOD_EXTRA_ARMOR_SHORT = "armor",
    ITEM_MOD_CRIT_RATING_SHORT = "critical_strike",
    ITEM_MOD_CRITICAL_STRIKE_RATING_SHORT = "critical_strike",
    ITEM_MOD_HASTE_RATING_SHORT = "haste",
    ITEM_MOD_MASTERY_RATING_SHORT = "mastery",
    ITEM_MOD_VERSATILITY = "versatility",
    ITEM_MOD_VERSATILITY_SHORT = "versatility",
    ITEM_MOD_AVOIDANCE_RATING_SHORT = "avoidance",
    ITEM_MOD_CR_AVOIDANCE_SHORT = "avoidance",
    ITEM_MOD_LIFESTEAL_RATING_SHORT = "leech",
    ITEM_MOD_LEECH_RATING_SHORT = "leech",
    ITEM_MOD_CR_LIFESTEAL_SHORT = "leech",
    ITEM_MOD_SPEED_RATING_SHORT = "speed",
    ITEM_MOD_CR_SPEED_SHORT = "speed",
}

local STAT_ORDER = {
    "strength", "agility", "intellect", "stamina", "armor",
    "critical_strike", "haste", "mastery", "versatility",
    "avoidance", "leech", "speed",
}
Parser.STAT_ORDER = STAT_ORDER

local function EmptyStats()
    local result = {}
    for _, key in ipairs(STAT_ORDER) do result[key] = 0 end
    return result
end
Parser.EmptyStats = EmptyStats

local function IsPlain(value)
    if type(issecretvalue) ~= "function" then return true end
    local ok, secret = pcall(issecretvalue, value)
    return not ok or not secret
end

function Parser.NormalizeStats(rawStats)
    local normalized = EmptyStats()
    local unmapped = {}
    if type(rawStats) ~= "table" then return normalized, unmapped end
    for key, value in pairs(rawStats) do
        if IsPlain(key) and IsPlain(value)
            and type(key) == "string" and type(value) == "number" then
            local target = Parser.STAT_KEY_MAP[key]
            if target then
                normalized[target] = normalized[target] + value
            else
                unmapped[key] = value
            end
        end
    end
    return normalized, unmapped
end

function Parser.AddStats(target, addition)
    target = target or EmptyStats()
    for _, key in ipairs(STAT_ORDER) do
        local value = addition and addition[key]
        if type(value) == "number" then target[key] = (target[key] or 0) + value end
    end
    return target
end

function Parser.ExtractItemString(link)
    if type(link) ~= "string" or link == "" then return nil end
    local itemString = link:match("|H(item:[^|]+)|h")
        or link:match("^(item:[^|%s]+)")
        or link:match("(item:[%-%d:]*)")
    if not itemString or not itemString:match("^item:%-?%d+") then return nil end
    return itemString
end

local function SplitPayload(payload)
    local fields = {}
    for field in (payload .. ":"):gmatch("(.-):") do
        fields[#fields + 1] = field
    end
    return fields
end

-- Construit uniquement une chaîne de requête pour C_Item.GetItemStats.
-- Champs Retail 2..6 : enchantement puis quatre gemmes. Les mettre à zéro
-- permet de demander les statistiques propres à l'instance (bonus IDs, craft,
-- contexte et upgrades conservés) sans toucher à l'objet réel ni à son lien
-- exporté. Si le format est incomplet, on refuse de prétendre l'avoir normalisé.
function Parser.BuildBaseStatsItemString(link)
    local itemString = Parser.ExtractItemString(link)
    if not itemString then return nil, "NOT_AN_ITEM_LINK" end
    local fields = SplitPayload(itemString:sub(6))
    if not tonumber(fields[1]) or #fields < 6 then
        return nil, "ITEM_STRING_TOO_SHORT"
    end
    for index = 2, 6 do fields[index] = "0" end
    return "item:" .. table.concat(fields, ":")
end

local function PositiveNumber(value)
    value = tonumber(value)
    if value and value > 0 then return value end
end

local function NonZeroNumber(value)
    value = tonumber(value)
    if value and value ~= 0 then return value end
end

function Parser.Parse(link)
    local itemString = Parser.ExtractItemString(link)
    if not itemString then return nil, "NOT_AN_ITEM_LINK" end

    local payload = itemString:sub(6)
    local fields = SplitPayload(payload)
    local itemID = PositiveNumber(fields[1])
    if not itemID then return nil, "INVALID_ITEM_ID" end

    local result = {
        item_string = itemString,
        raw_fields = JSON.Array(fields),
        item_id = itemID,
        -- Le deuxième champ du itemString décrit l'enchantement permanent de
        -- CETTE instance. Il ne doit jamais être reconstruit depuis l'itemID.
        enchant_id = PositiveNumber(fields[2]),
        gem_ids = JSON.Array({}),
        suffix_id = NonZeroNumber(fields[7]),
        unique_id = NonZeroNumber(fields[8]),
        link_level = PositiveNumber(fields[9]),
        specialization_id = PositiveNumber(fields[10]),
        modifiers_mask = NonZeroNumber(fields[11]),
        context = NonZeroNumber(fields[12]),
        bonus_ids = JSON.Array({}),
        raw_modifiers = JSON.Array({}),
        crafted_stats = JSON.Array({}),
        crafting_quality = nil,
        crafting_quality_modifier_id = nil,
        upgrade_id = nil,
        relic_bonus_ids = JSON.Array({}),
        crafter_guid = nil,
        truncated = false,
    }

    for index = 3, 6 do
        local gemID = PositiveNumber(fields[index])
        if gemID then result.gem_ids[#result.gem_ids + 1] = gemID end
    end

    local cursor = 13
    local bonusCount = tonumber(fields[cursor]) or 0
    if bonusCount < 0 then bonusCount = 0 end
    if bonusCount > 512 then bonusCount = 512; result.truncated = true end
    cursor = cursor + 1
    for _ = 1, bonusCount do
        if cursor > #fields then result.truncated = true; break end
        local bonusID = NonZeroNumber(fields[cursor])
        if bonusID then result.bonus_ids[#result.bonus_ids + 1] = bonusID end
        cursor = cursor + 1
    end

    local modifierCount = tonumber(fields[cursor]) or 0
    if modifierCount < 0 then modifierCount = 0 end
    if modifierCount > 256 then modifierCount = 256; result.truncated = true end
    cursor = cursor + 1
    for _ = 1, modifierCount do
        if cursor + 1 > #fields then result.truncated = true; break end
        local modifierType = tonumber(fields[cursor])
        local modifierValue = tonumber(fields[cursor + 1])
        result.raw_modifiers[#result.raw_modifiers + 1] = {
            type = modifierType or JSON.NULL,
            value = modifierValue or JSON.NULL,
        }
        if modifierType == MOD_CRAFT_STAT_1 or modifierType == MOD_CRAFT_STAT_2 then
            if modifierValue then result.crafted_stats[#result.crafted_stats + 1] = modifierValue end
        elseif modifierType == MOD_CRAFT_QUALITY and modifierValue then
            -- La valeur du modifier est un ID interne, pas nécessairement le rang
            -- 1..5 affiché au joueur. On l'exporte sans le transformer ; la qualité
            -- exacte vient de C_TradeSkillUI.GetItemCraftedQualityByItemInfo.
            result.crafting_quality_modifier_id = modifierValue
        elseif modifierType == MOD_UPGRADE_OBSOLETE and modifierValue then
            result.upgrade_id = modifierValue
        end
        cursor = cursor + 2
    end

    -- Trois listes historiques de bonus de reliques. Sur les objets modernes,
    -- ces champs sont généralement vides mais leur consommation garde le parseur
    -- aligné avec les éventuels champs additionnels suivants.
    for relicIndex = 1, 3 do
        local count = tonumber(fields[cursor]) or 0
        if count < 0 then count = 0 end
        if count > 128 then count = 128; result.truncated = true end
        cursor = cursor + 1
        local bonuses = JSON.Array({})
        for _ = 1, count do
            if cursor > #fields then result.truncated = true; break end
            local bonusID = NonZeroNumber(fields[cursor])
            if bonusID then bonuses[#bonuses + 1] = bonusID end
            cursor = cursor + 1
        end
        result.relic_bonus_ids[relicIndex] = bonuses
    end

    local crafterGUID = fields[cursor]
    if type(crafterGUID) == "string" and crafterGUID ~= "" then result.crafter_guid = crafterGUID end
    return result
end

function Parser.SelfTest()
    local link = "item:63470::::::::53:257::2:1:4198:2:28:1199:9:35:::::"
    local parsed, err = Parser.Parse(link)
    local enchanted = Parser.Parse(
        "item:1001:777:2001:2002:0:0:0:0:80:104:0:2:1:500:0:0:0:0:")
    local baseStatsLink = Parser.BuildBaseStatsItemString(
        "item:1001:777:2001:2002:0:0:0:0:80:104:0:2:1:500:0:0:0:0:")
    local ok = parsed and parsed.item_id == 63470 and parsed.context == 2
        and parsed.bonus_ids[1] == 4198 and #parsed.raw_modifiers == 2
        and parsed.raw_modifiers[1].type == 28 and parsed.raw_modifiers[1].value == 1199
        and enchanted and enchanted.item_id == 1001 and enchanted.enchant_id == 777
        and baseStatsLink
        and baseStatsLink:match("^item:1001:0:0:0:0:0:") ~= nil
    return not not ok, parsed or err
end
