-- ============================================================
-- Asylum Compagnon - GearCompassBonusRollHistory.lua
-- Historique prospectif, passif et par personnage des BONUS_ROLL_RESULT.
-- Aucun historique anterieur n'est reconstruit et aucune UI protegee n'est touchee.
-- ============================================================

local _, NS = ...

local History = {}
NS.GearCompassBonusRollHistory = History

local JSON = NS.GearCompassJSON
local Checksum = NS.GearCompassChecksum
local NULL = JSON.NULL
local MAX_RECORDS = 500
local LAST_CONTEXT_MAX_AGE = 30 * 60
local STORAGE_KEY = "gearcompassBonusRollHistory"

local currentEncounter
local lastEncounter
local observedKeystone

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

local function Plain(value, expectedType)
    if value == nil or IsSecret(value) then return nil end
    if expectedType and type(value) ~= expectedType then return nil end
    return value
end

local function OrNull(value)
    return value == nil and NULL or value
end

local function Now(api)
    local fn = type(api) == "table" and api.now or GetServerTime or time
    if type(fn) ~= "function" then return 0 end
    local ok, value = pcall(fn)
    value = ok and Plain(value, "number") or nil
    return value and math.max(0, math.floor(value)) or 0
end

local function PositiveInteger(value)
    value = Plain(value, "number")
    if not value or value <= 0 or value ~= math.floor(value) then return nil end
    return value
end

local function ItemIDFromLink(link)
    link = Plain(link, "string")
    if not link then return nil end
    return PositiveInteger(tonumber(
        link:match("|Hitem:(%d+)") or link:match("^item:(%d+)")))
end

local function SourceType(instanceType, mythicLevel)
    if PositiveInteger(mythicLevel) then return "mythic_plus" end
    if instanceType == "raid" then return "raid" end
    if instanceType == "party" then return "dungeon" end
    if instanceType == "scenario" then return "scenario" end
    if instanceType == "pvp" then return "pvp" end
    if instanceType == "arena" then return "arena" end
    if instanceType == "none" then return "world" end
    return nil
end

local function CleanStorage(storage, now)
    if type(storage) ~= "table" then return nil end
    if type(storage.trackingStartedAt) ~= "number"
        or storage.trackingStartedAt <= 0 then
        storage.trackingStartedAt = now
    else
        storage.trackingStartedAt = math.floor(storage.trackingStartedAt)
    end
    if type(storage.records) ~= "table" then storage.records = {} end
    local clean = {}
    for _, record in ipairs(storage.records) do
        if type(record) == "table" and type(record.event_id) == "string"
            and record.event_id ~= "" then
            clean[#clean + 1] = record
        end
    end
    local first = math.max(1, #clean - MAX_RECORDS + 1)
    storage.records = {}
    for index = first, #clean do
        storage.records[#storage.records + 1] = clean[index]
    end
    return storage
end

function History.GetStorage(create, api)
    local chardb = NS.chardb
    if type(chardb) ~= "table" then return nil end
    if type(chardb[STORAGE_KEY]) ~= "table" then
        if create == false then return nil end
        chardb[STORAGE_KEY] = {}
    end
    return CleanStorage(chardb[STORAGE_KEY], Now(api))
end

function History.CaptureContext(api)
    api = type(api) == "table" and api or {}
    local now = Now(api)
    local instanceName, instanceType, difficultyID, difficultyName, instanceID
    local getInstanceInfo = api.getInstanceInfo or GetInstanceInfo
    if type(getInstanceInfo) == "function" then
        local ok, rawName, rawType, rawDifficulty, rawDifficultyName,
            _, _, _, rawInstanceID = pcall(getInstanceInfo)
        if ok then
            instanceName = Plain(rawName, "string")
            instanceType = Plain(rawType, "string")
            difficultyID = PositiveInteger(rawDifficulty)
            difficultyName = Plain(rawDifficultyName, "string")
            instanceID = PositiveInteger(rawInstanceID)
        end
    end

    local mapID, mapName
    local getBestMap = api.getBestMapForUnit
        or (C_Map and C_Map.GetBestMapForUnit)
    if type(getBestMap) == "function" then
        local ok, value = pcall(getBestMap, "player")
        mapID = ok and PositiveInteger(value) or nil
    end
    local getMapInfo = api.getMapInfo or (C_Map and C_Map.GetMapInfo)
    if mapID and type(getMapInfo) == "function" then
        local ok, info = pcall(getMapInfo, mapID)
        if ok and type(info) == "table" and not IsSecret(info) then
            mapName = Plain(info.name, "string")
        end
    end

    local mythicLevel
    local getKeystone = api.getActiveKeystoneInfo
        or (C_ChallengeMode and C_ChallengeMode.GetActiveKeystoneInfo)
    if type(getKeystone) == "function" then
        local ok, value = pcall(getKeystone)
        mythicLevel = ok and PositiveInteger(value) or nil
    end
    if not mythicLevel and type(observedKeystone) == "table" then
        local age = now - (Plain(observedKeystone.observed_at, "number") or now)
        local sameInstance = not observedKeystone.instance_id
            or (instanceID and observedKeystone.instance_id == instanceID)
        if age >= 0 and age <= LAST_CONTEXT_MAX_AGE and sameInstance then
            mythicLevel = PositiveInteger(observedKeystone.mythic_level)
        end
    end

    local encounter = type(api.encounter) == "table" and api.encounter
        or currentEncounter or lastEncounter
    local encounterID, encounterName
    if type(encounter) == "table" then
        local age = now - (Plain(encounter.observed_at, "number") or now)
        local sameInstance = not encounter.instance_id
            or (instanceID and encounter.instance_id == instanceID)
        local sameDifficulty = not encounter.difficulty_id
            or (difficultyID and encounter.difficulty_id == difficultyID)
        if age >= 0 and age <= LAST_CONTEXT_MAX_AGE
            and sameInstance and sameDifficulty then
            encounterID = PositiveInteger(encounter.encounter_id)
            encounterName = Plain(encounter.encounter_name, "string")
        end
    end

    local context = SourceType(instanceType, mythicLevel)
    local difficulty = mythicLevel and ("+" .. tostring(mythicLevel))
        or difficultyName
    local destination = instanceName or mapName
    local confidence = "context_unknown"
    if context or destination or difficulty or encounterID then
        confidence = "context_partial"
    end
    if context and destination and difficulty then confidence = "event_observed" end

    return {
        map_id = mapID,
        instance_id = instanceID,
        instance_name = instanceName,
        destination = destination,
        encounter_id = encounterID,
        encounter_name = encounterName,
        difficulty_id = difficultyID,
        difficulty = difficulty,
        mythic_level = mythicLevel,
        source_type = context,
        context = context,
        confidence = confidence,
        context_confidence = confidence,
    }
end

local function EventDigest(parts)
    local payload = table.concat(parts, "\31")
    if Checksum and type(Checksum.Adler32Hex) == "function" then
        return Checksum.Adler32Hex(payload)
    end
    local sum = 0
    for index = 1, #payload do
        sum = (sum + string.byte(payload, index)) % 4294967296
    end
    return string.format("%08x", sum)
end

function History.BuildRecord(payload, context, occurredAt)
    payload = type(payload) == "table" and payload or {}
    context = type(context) == "table" and context or {}
    occurredAt = Plain(occurredAt, "number")
    occurredAt = occurredAt and math.max(0, math.floor(occurredAt)) or 0

    local typeIdentifier = Plain(payload.type_identifier, "string")
    local typeKey = typeIdentifier and typeIdentifier:lower() or ""
    local itemLink = Plain(payload.item_link, "string")
    if itemLink == "" then itemLink = nil end
    local itemID = ItemIDFromLink(itemLink)
    local currencyID = PositiveInteger(payload.currency_id)
    local quantity = Plain(payload.quantity, "number")
    if quantity then quantity = math.max(0, math.floor(quantity)) end

    local resultType
    if itemID then resultType = "item"
    elseif currencyID then resultType = "currency"
    elseif typeKey:find("money", 1, true) or typeKey:find("gold", 1, true) then
        resultType = "money"
    elseif typeKey ~= "" then resultType = typeKey
    else resultType = "unknown" end

    local contextType = Plain(context.context, "string")
        or Plain(context.source_type, "string")
    local difficulty = Plain(context.difficulty, "string")
    local mythicLevel = PositiveInteger(context.mythic_level)
    if not difficulty and mythicLevel then difficulty = "+" .. tostring(mythicLevel) end
    local confidence = Plain(context.confidence, "string")
        or Plain(context.context_confidence, "string") or "context_unknown"
    local record = {
        recorded_at = occurredAt,
        occurred_at = occurredAt,
        result_type = resultType,
        type_identifier = typeIdentifier,
        item_id = itemID,
        item_link = itemID and itemLink or nil,
        destination = Plain(context.destination, "string")
            or Plain(context.instance_name, "string"),
        instance_name = Plain(context.instance_name, "string"),
        encounter_name = Plain(context.encounter_name, "string"),
        difficulty = difficulty,
        context = contextType,
        confidence = confidence,
        currency_id = currencyID,
        money = resultType == "money" and quantity or nil,
        quantity = quantity,
        loot_spec_id = PositiveInteger(payload.spec_id),
        map_id = PositiveInteger(context.map_id),
        instance_id = PositiveInteger(context.instance_id),
        encounter_id = PositiveInteger(context.encounter_id),
        difficulty_id = PositiveInteger(context.difficulty_id),
        mythic_level = mythicLevel,
        source_type = contextType,
        context_confidence = confidence,
        is_secondary_result = Plain(payload.is_secondary_result, "boolean"),
        corrupted = Plain(payload.corrupted, "boolean"),
    }
    local parts = {
        tostring(record.recorded_at), tostring(record.type_identifier or ""),
        tostring(record.item_link or ""), tostring(record.currency_id or ""),
        tostring(record.quantity or ""), tostring(record.loot_spec_id or ""),
        tostring(record.destination or ""), tostring(record.instance_id or ""),
        tostring(record.encounter_id or ""), tostring(record.difficulty or ""),
        tostring(record.difficulty_id or ""), tostring(record.context or ""),
        tostring(record.is_secondary_result or false), tostring(record.corrupted or false),
    }
    record.event_id = "bonus-roll:" .. tostring(record.recorded_at)
        .. ":" .. tostring(EventDigest(parts) or "00000000")
    return record
end

function History.AppendRecord(record, storage)
    storage = type(storage) == "table" and CleanStorage(storage, Now())
        or History.GetStorage(true)
    if type(storage) ~= "table" or type(record) ~= "table"
        or type(record.event_id) ~= "string" then
        return false, "BONUS_ROLL_STORAGE_UNAVAILABLE"
    end
    for _, existing in ipairs(storage.records) do
        if type(existing) == "table" and existing.event_id == record.event_id then
            return false, "BONUS_ROLL_DUPLICATE"
        end
    end
    storage.records[#storage.records + 1] = record
    while #storage.records > MAX_RECORDS do table.remove(storage.records, 1) end
    local data = NS.GearCompassDB and NS.GearCompassDB.Get
        and NS.GearCompassDB.Get()
    if data then data.lastExport = nil end
    return true
end

function History.RecordBonusRoll(typeIdentifier, itemLink, quantity, specID, sex,
        personalLootToast, currencyID, isSecondaryResult, corrupted, api)
    api = type(api) == "table" and api or {}
    local occurredAt = Now(api)
    local context = type(api.context) == "table" and api.context
        or History.CaptureContext(api)
    local record = History.BuildRecord({
        type_identifier = typeIdentifier,
        item_link = itemLink,
        quantity = quantity,
        spec_id = specID,
        currency_id = currencyID,
        is_secondary_result = isSecondaryResult,
        corrupted = corrupted,
    }, context, occurredAt)
    local added, reason = History.AppendRecord(record, api.storage)
    return added, reason, record
end

local function PublicRecord(record)
    record = type(record) == "table" and record or {}
    return {
        event_id = OrNull(Plain(record.event_id, "string")),
        item_id = OrNull(PositiveInteger(record.item_id)),
        item_link = OrNull(Plain(record.item_link, "string")),
        destination = OrNull(Plain(record.destination, "string")),
        instance_name = OrNull(Plain(record.instance_name, "string")),
        encounter_name = OrNull(Plain(record.encounter_name, "string")),
        difficulty = OrNull(Plain(record.difficulty, "string")),
        context = OrNull(Plain(record.context, "string")),
        recorded_at = OrNull(Plain(record.recorded_at, "number")
            or Plain(record.occurred_at, "number")),
        confidence = Plain(record.confidence, "string")
            or Plain(record.context_confidence, "string") or "context_unknown",
        result_type = OrNull(Plain(record.result_type, "string")),
        type_identifier = OrNull(Plain(record.type_identifier, "string")),
        quantity = OrNull(Plain(record.quantity, "number")),
        currency_id = OrNull(PositiveInteger(record.currency_id)),
        money = OrNull(Plain(record.money, "number")),
        loot_spec_id = OrNull(PositiveInteger(record.loot_spec_id)),
        map_id = OrNull(PositiveInteger(record.map_id)),
        instance_id = OrNull(PositiveInteger(record.instance_id)),
        encounter_id = OrNull(PositiveInteger(record.encounter_id)),
        difficulty_id = OrNull(PositiveInteger(record.difficulty_id)),
        mythic_level = OrNull(PositiveInteger(record.mythic_level)),
        is_secondary_result = OrNull(Plain(record.is_secondary_result, "boolean")),
        corrupted = OrNull(Plain(record.corrupted, "boolean")),
    }
end

function History.GetExport(storage)
    storage = type(storage) == "table" and CleanStorage(storage, Now())
        or History.GetStorage(true)
    local records = JSON.Array({})
    for _, record in ipairs(storage and storage.records or {}) do
        records[#records + 1] = PublicRecord(record)
    end
    return {
        bonus_roll_history_complete = false,
        bonus_roll_history_scope = "prospective",
        bonus_roll_history_started_at = OrNull(
            storage and PositiveInteger(storage.trackingStartedAt)),
        bonus_roll_history = records,
    }
end

function History.Initialize()
    return History.GetStorage(true)
end

function History.SelfTest()
    local storage = { trackingStartedAt = 1784130000, records = {} }
    local raidContext = {
        map_id = 2405, instance_id = 1307, instance_name = "Test Raid",
        destination = "Test Raid", encounter_id = 2734,
        encounter_name = "Test Boss", difficulty_id = 15,
        difficulty = "Heroic", context = "raid", confidence = "event_observed",
    }
    local api = {
        now = function() return 1784134655 end,
        context = raidContext, storage = storage,
    }
    local itemAdded, _, item = History.RecordBonusRoll("item",
        "|cffa335ee|Hitem:251115::::::::90:::::|h[Test]|h|r", 1, 105,
        nil, false, nil, false, false, api)
    local duplicateAdded, duplicateReason = History.RecordBonusRoll("item",
        "|cffa335ee|Hitem:251115::::::::90:::::|h[Test]|h|r", 1, 105,
        nil, false, nil, false, false, api)

    api.now = function() return 1784134656 end
    api.context = {
        instance_id = 1307, instance_name = "Test Dungeon",
        destination = "Test Dungeon", difficulty_id = 8,
        difficulty = "+10", mythic_level = 10,
        context = "mythic_plus", confidence = "event_observed",
    }
    local plusTenAdded, _, plusTen = History.RecordBonusRoll("item",
        "|Hitem:251116|h[First key]|h", 1, 105,
        nil, false, nil, false, false, api)
    -- Même seconde, même objet et même quantité : seule la difficulté change.
    api.now = function() return 1784134656 end
    api.context.difficulty = "+12"
    api.context.mythic_level = 12
    local plusTwelveAdded, _, plusTwelve = History.RecordBonusRoll("item",
        "|Hitem:251116|h[First key]|h", 1, 105,
        nil, false, nil, false, false, api)

    api.now = function() return 1784134658 end
    api.context = {}
    local currencyAdded, _, currency = History.RecordBonusRoll("currency", nil,
        3, 105, nil, false, 3418, false, false, api)
    local incompleteContext = History.CaptureContext({
        now = function() return 1784134659 end,
        getInstanceInfo = false, getBestMapForUnit = false,
        getActiveKeystoneInfo = false,
    })
    local capturedMythicContext = History.CaptureContext({
        now = function() return 1784134660 end,
        getInstanceInfo = function()
            return "Validation Dungeon", "party", 8, "Mythic Keystone",
                5, nil, false, 1307
        end,
        getBestMapForUnit = function() return 2405 end,
        getActiveKeystoneInfo = function() return 12 end,
    })

    local capped = { trackingStartedAt = 1, records = {} }
    for index = 1, MAX_RECORDS do
        capped.records[index] = { event_id = "old:" .. index, recorded_at = index }
    end
    local capRecord = History.BuildRecord({ type_identifier = "money", quantity = 1 },
        {}, 1784134999)
    local capAdded = History.AppendRecord(capRecord, capped)
    local exported = History.GetExport(storage)
    local reloadExport = History.GetExport(storage)
    local otherCharacterStorage = { trackingStartedAt = 1784140000, records = {} }
    local otherExport = History.GetExport(otherCharacterStorage)
    local encoded = JSON.Encode(exported)

    local ok = itemAdded == true and duplicateAdded == false
        and duplicateReason == "BONUS_ROLL_DUPLICATE" and #storage.records == 4
        and item.item_id == 251115 and item.destination == "Test Raid"
        and item.encounter_name == "Test Boss" and item.difficulty == "Heroic"
        and item.confidence == "event_observed"
        and plusTenAdded and plusTwelveAdded
        and plusTen.difficulty == "+10" and plusTwelve.difficulty == "+12"
        and plusTen.event_id ~= plusTwelve.event_id
        and currencyAdded and currency.result_type == "currency"
        and currency.currency_id == 3418
        and incompleteContext.confidence == "context_unknown"
        and capturedMythicContext.context == "mythic_plus"
        and capturedMythicContext.difficulty == "+12"
        and capturedMythicContext.destination == "Validation Dungeon"
        and capturedMythicContext.confidence == "event_observed"
        and capAdded == true and #capped.records == MAX_RECORDS
        and capped.records[1].event_id == "old:2"
        and exported.bonus_roll_history_scope == "prospective"
        and exported.bonus_roll_history_complete == false
        and exported.bonus_roll_history_started_at == 1784130000
        and #exported.bonus_roll_history == 4
        and #reloadExport.bonus_roll_history == 4
        and #otherExport.bonus_roll_history == 0
        and type(encoded) == "string" and not encoded:find("%z")
    return not not ok, {
        item = item, plus_ten = plusTen, plus_twelve = plusTwelve,
        currency = currency, incomplete_context = incompleteContext,
        captured_mythic_context = capturedMythicContext,
        exported = exported, reload_export = reloadExport,
        other_character_export = otherExport,
        duplicate_reason = duplicateReason, capped_count = #capped.records,
    }
end

local frame = CreateFrame and CreateFrame("Frame")
if frame then
    frame:RegisterEvent("PLAYER_ENTERING_WORLD")
    frame:RegisterEvent("ENCOUNTER_START")
    frame:RegisterEvent("ENCOUNTER_END")
    frame:RegisterEvent("CHALLENGE_MODE_START")
    frame:RegisterEvent("CHALLENGE_MODE_COMPLETED")
    frame:RegisterEvent("CHALLENGE_MODE_RESET")
    frame:RegisterEvent("BONUS_ROLL_RESULT")
    frame:SetScript("OnEvent", function(_, event, ...)
        if event == "PLAYER_ENTERING_WORLD" then
            currentEncounter = nil
            lastEncounter = nil
            observedKeystone = nil
        elseif event == "ENCOUNTER_START" then
            local encounterID, encounterName, difficultyID = ...
            local _, _, _, _, _, _, _, instanceID = GetInstanceInfo()
            currentEncounter = {
                encounter_id = PositiveInteger(encounterID),
                encounter_name = Plain(encounterName, "string"),
                difficulty_id = PositiveInteger(difficultyID),
                instance_id = PositiveInteger(instanceID),
                observed_at = Now(),
            }
            lastEncounter = currentEncounter
        elseif event == "ENCOUNTER_END" then
            local encounterID, encounterName, difficultyID = ...
            local _, _, _, _, _, _, _, instanceID = GetInstanceInfo()
            lastEncounter = {
                encounter_id = PositiveInteger(encounterID),
                encounter_name = Plain(encounterName, "string"),
                difficulty_id = PositiveInteger(difficultyID),
                instance_id = PositiveInteger(instanceID),
                observed_at = Now(),
            }
            currentEncounter = nil
        elseif event == "CHALLENGE_MODE_START" then
            local level
            if C_ChallengeMode and C_ChallengeMode.GetActiveKeystoneInfo then
                local ok, value = pcall(C_ChallengeMode.GetActiveKeystoneInfo)
                level = ok and PositiveInteger(value) or nil
            end
            local _, _, _, _, _, _, _, instanceID = GetInstanceInfo()
            observedKeystone = {
                mythic_level = level,
                instance_id = PositiveInteger(instanceID),
                observed_at = Now(),
            }
        elseif event == "CHALLENGE_MODE_COMPLETED" then
            if observedKeystone then observedKeystone.observed_at = Now() end
        elseif event == "CHALLENGE_MODE_RESET" then
            observedKeystone = nil
        elseif event == "BONUS_ROLL_RESULT" then
            pcall(History.RecordBonusRoll, ...)
        end
    end)
end
