local _, ns = ...
local L = ns.L or {}

local Util = ns.Util or {}
local getWeeklyResetInfo = Util.GetWeeklyResetInfo or function()
    return 0, time()
end

local Renown = {}
ns.Renown = Renown

local HISTORY_LIMIT = 80
local SCAN_THROTTLE_SECONDS = 8
local LEVEL_CAP = 90
local CURRENT_RENOWN_DB_VERSION = 3
local CURRENT_RENOWN_ACTIVITY_SCHEMA_VERSION = 3
local RENOWN_TOAST_STYLE_DEFAULT = "normal"
local RENOWN_TOAST_ANIMATION_DEFAULT = "soft"
local RENOWN_TOAST_SCALE_DEFAULT = 1
local RENOWN_TOAST_SCALE_MIN = 0.75
local RENOWN_TOAST_SCALE_MAX = 1.35
local RENOWN_TOAST_SCALE_STEP = 0.05
local RENOWN_TOAST_STYLES = {
    normal = true,
    clean = true,
    simple = true,
}
local RENOWN_TOAST_ANIMATIONS = {
    soft = true,
    fade = true,
    pop = true,
}

local RANKS = {
    { level = 1, key = "peon", label = "Peon", assetIndex = 1 },
    { level = 4, key = "initiate", label = "Initiate", assetIndex = 2 },
    { level = 7, key = "scout", label = "Scout", assetIndex = 3 },
    { level = 10, key = "grunt", label = "Grunt", assetIndex = 4 },
    { level = 13, key = "footman", label = "Footman", assetIndex = 5 },
    { level = 16, key = "raider", label = "Raider", assetIndex = 6 },
    { level = 19, key = "veteran", label = "Veteran", assetIndex = 7 },
    { level = 22, key = "sergeant", label = "Sergeant", assetIndex = 8 },
    { level = 25, key = "stone_guard", label = "Stone Guard", assetIndex = 9 },
    { level = 28, key = "blood_guard", label = "Blood Guard", assetIndex = 10 },
    { level = 31, key = "legionnaire", label = "Legionnaire", assetIndex = 11 },
    { level = 34, key = "champion", label = "Champion", assetIndex = 12 },
    { level = 37, key = "centurion", label = "Centurion", assetIndex = 13 },
    { level = 40, key = "knight", label = "Knight", assetIndex = 14 },
    { level = 43, key = "commander", label = "Commander", assetIndex = 15 },
    { level = 46, key = "marshal", label = "Marshal", assetIndex = 16 },
    { level = 49, key = "general", label = "General", assetIndex = 17 },
    { level = 54, key = "high_general", label = "High General", assetIndex = 18 },
    { level = 60, key = "warlord", label = "Warlord", assetIndex = 19 },
    { level = 70, key = "high_warlord", label = "High Warlord", assetIndex = 20 },
    { level = 76, key = "hero", label = "Hero", assetIndex = 21 },
    { level = 82, key = "mythic_hero", label = "Mythic Hero", assetIndex = 22 },
    { level = 88, key = "ascendant", label = "Ascendant", assetIndex = 23 },
    { level = 90, key = "titanforged", label = "Titanforged", assetIndex = 24 },
}

local WARBAND_RANKS = {
    { level = 1, key = "camp", label = "Camp", assetIndex = 1 },
    { level = 10, key = "troop", label = "Troop", assetIndex = 2 },
    { level = 20, key = "vanguard", label = "Vanguard", assetIndex = 3 },
    { level = 30, key = "company", label = "Company", assetIndex = 4 },
    { level = 40, key = "legion", label = "Legion", assetIndex = 5 },
    { level = 50, key = "campaign", label = "Campaign", assetIndex = 6 },
    { level = 60, key = "starbound", label = "Starbound", assetIndex = 7 },
    { level = 70, key = "eternal", label = "Eternal", assetIndex = 8 },
    { level = 80, key = "ascendant", label = "Ascendant", assetIndex = 9 },
    { level = 90, key = "legendary", label = "Legendary", assetIndex = 10 },
}

local SOURCE_DEFAULT_XP = {
    vault_slot = 10,
    vault_goal_slot = 6,
    vault_row = 35,
    pve_weekly = 22,
    pve_daily = 7,
    pve_darkmoon = 2,
    pve_events = 16,
    pve_world = 16,
    pve_delves = 16,
    pve_prey = 12,
    pve_rares = 1,
    pvp_weekly = 18,
    professions = 16,
    profession_knowledge = 1,
    housing_weekly = 24,
    housing_task = 12,
    housing_milestone = 30,
    housing_level = 75,
}

local EVENT_XP = {
    daily_wanted_harandar = 6,
    world_boss_weekly = 20,
    special_assignments = 16,
    world_key_shards = 22,
    world_dundun_shards = 14,
}

local PREY_DIFFICULTY_XP = {
    normal = 4,
    hard = 6,
    nightmare = 8,
}

local RenownRuntime = {
    lastScans = {},
}
ns.RenownRuntime = RenownRuntime

local function clamp(value, minimum, maximum)
    value = tonumber(value) or 0
    if value < minimum then
        return minimum
    end
    if value > maximum then
        return maximum
    end
    return value
end

local function normalizeRenownToastScale(scale)
    scale = clamp(scale or RENOWN_TOAST_SCALE_DEFAULT, RENOWN_TOAST_SCALE_MIN, RENOWN_TOAST_SCALE_MAX)
    return math.floor((scale * 100) + 0.5) / 100
end

local function getNow()
    return type(time) == "function" and time() or 0
end

local function getCharacterKey(character)
    if type(character) == "table" and type(character.key) == "string" and character.key ~= "" then
        return character.key
    end
    if ns.GetCurrentCharacterInfo then
        local info = ns:GetCurrentCharacterInfo()
        return info and info.key or nil
    end
    return nil
end

local function isRenownSystemEnabled()
    if ns.IsMainTabEnabled then
        return ns:IsMainTabEnabled("chronicle") == true
    end

    local hiddenTabs = ns.db and ns.db.ui and ns.db.ui.hiddenMainTabs
    return not (type(hiddenTabs) == "table" and hiddenTabs.chronicle == true)
end

function ns:IsRenownSystemEnabled()
    return isRenownSystemEnabled()
end

function ns:OnRenownSystemDisabled()
    RenownRuntime.lastScans = {}
    local renownDB = self.EnsureRenownDB and self:EnsureRenownDB() or nil
    for _, profile in pairs(type(renownDB) == "table" and type(renownDB.characters) == "table" and renownDB.characters or {}) do
        if type(profile) == "table" then
            profile.baselineReady = false
        end
    end
    if self.UI and self.UI.HideRenownToast then
        self.UI:HideRenownToast()
    end
end

local function normalizeToken(value)
    value = string.lower(tostring(value or ""))
    value = value:gsub("[^%w]+", "_")
    value = value:gsub("^_+", ""):gsub("_+$", "")
    return value ~= "" and value or "activity"
end

local function getWeeklyResetKey()
    local _, resetAt = getWeeklyResetInfo()
    resetAt = tonumber(resetAt) or 0
    if resetAt <= 0 then
        resetAt = getNow()
    end
    return "week:" .. tostring(math.floor(resetAt))
end

local function getDailyResetKey()
    local seconds = ns.GetDailyResetSeconds and ns:GetDailyResetSeconds() or 0
    local resetAt = getNow() + math.max(0, tonumber(seconds) or 0)
    if resetAt <= 0 then
        resetAt = getNow()
    end
    return "day:" .. tostring(math.floor(resetAt))
end

local function getResetKey(kind)
    if kind == "daily" then
        return getDailyResetKey()
    end
    if kind == "darkmoon" then
        return ns.GetDarkmoonFaireResetKey and ns:GetDarkmoonFaireResetKey() or getWeeklyResetKey()
    end
    if kind == "permanent" then
        return "permanent"
    end
    return getWeeklyResetKey()
end

local function ensureProfile(profile)
    profile = type(profile) == "table" and profile or {}
    profile.totalXP = math.max(0, tonumber(profile.totalXP) or 0)
    profile.ledger = type(profile.ledger) == "table" and profile.ledger or {}
    profile.history = type(profile.history) == "table" and profile.history or {}
    profile.counters = type(profile.counters) == "table" and profile.counters or {}
    profile.baselineReady = profile.baselineReady == true
    profile.activitySchemaVersion = math.max(0, math.floor(tonumber(profile.activitySchemaVersion) or 0))
    return profile
end

local function resetRenownProgressForBaseline(renownDB)
    if type(renownDB) ~= "table" then
        return
    end

    renownDB.warband = ensureProfile({})
    renownDB.characters = {}
end

function ns:EnsureRenownDB()
    if not self.db and type(VaultloomNextDB) == "table" then
        self.db = VaultloomNextDB
    end
    if not self.db and self.EnsureDB then
        self:EnsureDB()
    end

    local db = self.db
    if type(db) ~= "table" then
        return nil
    end

    db.renown = type(db.renown) == "table" and db.renown or {}
    local savedVersion = tonumber(db.renown.version) or 0
    if savedVersion < CURRENT_RENOWN_DB_VERSION then
        resetRenownProgressForBaseline(db.renown)
    end
    db.renown.version = CURRENT_RENOWN_DB_VERSION
    db.renown.warband = ensureProfile(db.renown.warband)
    db.renown.characters = type(db.renown.characters) == "table" and db.renown.characters or {}
    db.renown.settings = type(db.renown.settings) == "table" and db.renown.settings or {}
    db.renown.settings.showPortraitFrame = db.renown.settings.showPortraitFrame ~= false
    db.renown.settings.showRenownBadge = db.renown.settings.showRenownBadge ~= false
    db.renown.settings.showWarbandBadge = db.renown.settings.showWarbandBadge ~= false
    db.renown.settings.showRenownToast = db.renown.settings.showRenownToast ~= false
    db.renown.settings.toastPosition = type(db.renown.settings.toastPosition) == "table" and db.renown.settings.toastPosition or nil
    db.renown.settings.toastStyle = RENOWN_TOAST_STYLES[db.renown.settings.toastStyle] and db.renown.settings.toastStyle or RENOWN_TOAST_STYLE_DEFAULT
    db.renown.settings.toastAnimation = RENOWN_TOAST_ANIMATIONS[db.renown.settings.toastAnimation] and db.renown.settings.toastAnimation or RENOWN_TOAST_ANIMATION_DEFAULT
    db.renown.settings.toastScale = normalizeRenownToastScale(db.renown.settings.toastScale)
    db.renown.settings.characterFrames = type(db.renown.settings.characterFrames) == "table" and db.renown.settings.characterFrames or {}
    db.renown.settings.characterBadges = type(db.renown.settings.characterBadges) == "table" and db.renown.settings.characterBadges or {}

    db.ui = type(db.ui) == "table" and db.ui or {}
    db.ui.renownScope = (db.ui.renownScope == "warband") and "warband" or "character"

    return db.renown
end

local function getCharacterProfile(renownDB, characterKey)
    if not renownDB or type(characterKey) ~= "string" or characterKey == "" then
        return nil
    end
    renownDB.characters[characterKey] = ensureProfile(renownDB.characters[characterKey])
    return renownDB.characters[characterKey]
end

local function getCharacterLevelXP(level)
    level = math.max(1, math.floor(tonumber(level) or 1))
    return 1800 + ((level - 1) * 525) + math.floor(((level - 1) ^ 1.62) * 220)
end

local function getLevelXP(level, scope)
    local base = getCharacterLevelXP(level)
    if scope == "warband" then
        local levelNumber = math.max(1, math.floor(tonumber(level) or 1))
        local multiplier = 4.25 + (math.min(levelNumber - 1, 109) * 0.035)
        return math.floor(base * multiplier)
    end
    return base
end

local function getTotalXPForLevel(level, scope)
    level = math.max(1, math.floor(tonumber(level) or 1))
    local total = 0
    for currentLevel = 1, level - 1 do
        total = total + getLevelXP(currentLevel, scope)
    end
    return total
end

local function getLevelState(totalXP, scope)
    totalXP = math.max(0, tonumber(totalXP) or 0)
    local level = 1
    local spent = 0

    while level < LEVEL_CAP do
        local needed = getLevelXP(level, scope)
        if totalXP < spent + needed then
            break
        end
        spent = spent + needed
        level = level + 1
    end

    if level >= LEVEL_CAP and totalXP >= spent + getLevelXP(level, scope) then
        local paragonXP = totalXP - getTotalXPForLevel(LEVEL_CAP, scope)
        local paragonSize = getLevelXP(LEVEL_CAP, scope)
        local paragonLevel = math.floor(paragonXP / math.max(1, paragonSize)) + 1
        local paragonProgress = paragonXP % math.max(1, paragonSize)
        return {
            level = LEVEL_CAP,
            paragon = paragonLevel,
            currentXP = paragonProgress,
            neededXP = paragonSize,
            ratio = paragonProgress / math.max(1, paragonSize),
            totalXP = totalXP,
        }
    end

    local needed = getLevelXP(level, scope)
    local current = totalXP - spent
    return {
        level = level,
        paragon = nil,
        currentXP = current,
        neededXP = needed,
        ratio = current / math.max(1, needed),
        totalXP = totalXP,
    }
end

local function getRanksForScope(scope)
    return scope == "warband" and WARBAND_RANKS or RANKS
end

local function getRankLabel(rank, scope)
    if type(rank) ~= "table" then
        return ""
    end

    local prefix = scope == "warband" and "RENOWN_WARBAND_RANK_" or "RENOWN_RANK_"
    return rank.label or L[prefix .. string.upper(rank.key or "")] or ""
end

local function getRankForLevel(levelState, scope)
    local level = tonumber(levelState and levelState.level) or 1
    local ranks = getRanksForScope(scope)
    local rank = ranks[1]
    for _, candidate in ipairs(ranks) do
        if level >= candidate.level then
            rank = candidate
        end
    end

    if levelState and levelState.paragon then
        return {
            key = "paragon",
            label = string.format(L.RENOWN_PARAGON_FORMAT or "Paragon %d", levelState.paragon),
            minLevel = LEVEL_CAP,
            assetIndex = #ranks,
        }
    end

    return {
        key = rank.key,
        label = getRankLabel(rank, scope),
        minLevel = rank.level,
        assetIndex = rank.assetIndex,
    }
end

local function pushHistory(profile, entry)
    if type(profile) ~= "table" or type(entry) ~= "table" then
        return
    end

    table.insert(profile.history, 1, entry)
    while #profile.history > HISTORY_LIMIT do
        table.remove(profile.history)
    end
end

local function isEntryComplete(entry)
    return type(entry) == "table" and (entry.completed == true or entry.status == "complete")
end

local function makeActivityKey(prefix, resetKind, rawKey)
    return table.concat({
        getResetKey(resetKind),
        normalizeToken(prefix),
        normalizeToken(rawKey),
    }, ":")
end

local function addActivity(activities, source, resetKind, rawKey, label, amount, options)
    if type(activities) ~= "table" then
        return
    end

    rawKey = tostring(rawKey or label or source or "activity")
    label = tostring(label or rawKey)
    amount = math.max(1, math.floor(tonumber(amount) or SOURCE_DEFAULT_XP[source] or 1))

    local activity = {
        source = source,
        resetKind = resetKind or "weekly",
        key = makeActivityKey(source, resetKind or "weekly", rawKey),
        rawKey = rawKey,
        label = label,
        amount = amount,
    }
    if type(options) == "table" and options.accountWide == true then
        activity.accountWide = true
    end

    activities[#activities + 1] = activity
end

local function getRowRenownOptions(row, entry)
    if type(entry) == "table" and (entry.accountWide == true or entry.renownAccountWide == true) then
        return { accountWide = true }
    end
    if type(row) == "table" and (row.accountWide == true or row.renownAccountWide == true) then
        return { accountWide = true }
    end
    return nil
end

local function addRows(activities, source, resetKind, rows, defaultAmount)
    for index, row in ipairs(type(rows) == "table" and rows or {}) do
        local entry = row and row.entry
        if isEntryComplete(entry) then
            local rawKey = entry.key or row.id or row.key or row.label or index
            local amount = entry.renownXP or row.renownXP or EVENT_XP[rawKey] or defaultAmount or SOURCE_DEFAULT_XP[source] or 8
            addActivity(activities, source, resetKind, rawKey, row.label or entry.label or rawKey, amount, getRowRenownOptions(row, entry))
        end
    end
end

local function addRowsWithEntryReset(activities, source, rows, defaultAmount)
    for index, row in ipairs(type(rows) == "table" and rows or {}) do
        local entry = row and row.entry
        if isEntryComplete(entry) then
            local rawKey = entry.key or row.id or row.key or row.label or index
            local resetKind = entry.renownResetKind or entry.resetType or "weekly"
            local amount = entry.renownXP or row.renownXP or EVENT_XP[rawKey] or defaultAmount or SOURCE_DEFAULT_XP[source] or 1
            addActivity(activities, source, resetKind, rawKey, row.label or entry.label or rawKey, amount, getRowRenownOptions(row, entry))
        end
    end
end

local function addCountSteps(activities, source, resetKind, rawKey, label, count, maxCount, amountPerStep, options)
    count = math.max(0, math.floor(tonumber(count) or 0))
    maxCount = math.max(0, math.floor(tonumber(maxCount) or count))
    if maxCount > 0 then
        count = math.min(count, maxCount)
    end
    amountPerStep = math.max(1, math.floor(tonumber(amountPerStep) or 1))

    for step = 1, count do
        addActivity(
            activities,
            source,
            resetKind,
            tostring(rawKey or source) .. "_step_" .. tostring(step),
            string.format("%s %d/%d", tostring(label or rawKey or source), step, maxCount > 0 and maxCount or count),
            amountPerStep,
            options
        )
    end
end

local function safeBuild(builder, ...)
    if type(builder) ~= "function" then
        return nil
    end

    local ok, result = pcall(builder, ...)
    if ok and type(result) == "table" then
        return result
    end
    return nil
end

local function collectVaultActivities(activities, snapshot)
    local rows = type(snapshot) == "table" and type(snapshot.rows) == "table" and snapshot.rows or nil
    if not rows then
        return false
    end

    for rowKey, row in pairs(rows) do
        local label = row.label or rowKey
        local completedCount = tonumber(row.completedCount) or 0
        for index, threshold in ipairs(row.thresholds or {}) do
            threshold = tonumber(threshold) or 0
            if threshold > 0 and completedCount >= threshold then
                addActivity(
                    activities,
                    "vault_slot",
                    "weekly",
                    tostring(rowKey) .. "_slot_" .. tostring(index),
                    string.format(L.RENOWN_ACTIVITY_VAULT_SLOT or "%s vault slot %d unlocked", label, index),
                    SOURCE_DEFAULT_XP.vault_slot
                )
            end
        end

        for index, goalSlot in ipairs(type(row.goal and row.goal.slots) == "table" and row.goal.slots or {}) do
            local threshold = tonumber(goalSlot and goalSlot.threshold) or tonumber(row.thresholds and row.thresholds[index]) or 0
            local count = tonumber(goalSlot and goalSlot.count) or tonumber(row.goal and row.goal.displayCount) or 0
            if threshold > 0 and count >= threshold then
                addActivity(
                    activities,
                    "vault_goal_slot",
                    "weekly",
                    tostring(rowKey) .. "_goal_slot_" .. tostring(index),
                    string.format(L.RENOWN_ACTIVITY_VAULT_GOAL_SLOT or "%s vault slot %d max reward reached", label, index),
                    SOURCE_DEFAULT_XP.vault_goal_slot
                )
            end
        end

        local maxThreshold = tonumber(row.goal and row.goal.maxThreshold) or tonumber(row.thresholds and row.thresholds[#row.thresholds]) or 0
        local displayCount = tonumber(row.goal and row.goal.displayCount) or completedCount
        if maxThreshold > 0 and displayCount >= maxThreshold then
            addActivity(
                activities,
                "vault_row",
                "weekly",
                tostring(rowKey) .. "_max",
                string.format(L.RENOWN_ACTIVITY_VAULT_MAX or "%s vault row maxed", label),
                SOURCE_DEFAULT_XP.vault_row
            )
        end
    end

    return true
end

local function ensurePreyCounters(profile)
    if type(profile) ~= "table" then
        return nil
    end

    profile.counters = type(profile.counters) == "table" and profile.counters or {}
    profile.counters.prey = type(profile.counters.prey) == "table" and profile.counters.prey or {}

    local counter = profile.counters.prey
    local resetKey = getWeeklyResetKey()
    if counter.resetKey ~= resetKey then
        counter.resetKey = resetKey
        counter.difficulties = {}
        counter.totalCapReached = nil
    end
    counter.difficulties = type(counter.difficulties) == "table" and counter.difficulties or {}

    return counter
end

local function collectPreyActivities(activities, view, profile, baseline)
    if type(view) ~= "table" then
        return false
    end

    addRows(activities, "pve_prey", "weekly", {
        { entry = view.weeklyQuest, label = view.weeklyLabel or (L.PVE_PREY_WEEKLY_NAME or "Prey weekly") },
    }, 22)

    local preyCounter = ensurePreyCounters(profile)
    local preyCounts = preyCounter and preyCounter.difficulties or nil
    for key, difficulty in pairs(type(view.difficulties) == "table" and view.difficulties or {}) do
        local entry = difficulty and difficulty.entry
        local count = math.max(0, math.floor(tonumber(entry and entry.count) or 0))
        local cap = math.max(0, math.floor(tonumber(entry and entry.cap) or 0))
        if entry and entry.unlocked ~= false and preyCounts then
            local preyKey = tostring(key)
            local previousCount = tonumber(preyCounts[preyKey])
            local label = string.format(L.RENOWN_ACTIVITY_PREY_DIFFICULTY or "Prey %s completed", difficulty.label or key)
            if previousCount == nil or baseline == true then
                preyCounts[preyKey] = count
            elseif count > previousCount then
                for step = previousCount + 1, count do
                    addActivity(
                        activities,
                        "pve_prey",
                        "weekly",
                        "difficulty_" .. preyKey .. "_step_" .. tostring(step),
                        string.format("%s %d/%d", label, step, cap > 0 and cap or count),
                        PREY_DIFFICULTY_XP[key] or SOURCE_DEFAULT_XP.pve_prey
                    )
                end
                preyCounts[preyKey] = count
            elseif count < previousCount then
                preyCounts[preyKey] = count
            end
        end
    end

    if tonumber(view.totalCap) and tonumber(view.totalCap) > 0 then
        local totalText = tostring(view.totalValue or "")
        local current = tonumber(totalText:match("^(%d+)")) or 0
        local totalCapReached = current >= tonumber(view.totalCap)
        if preyCounter and (preyCounter.totalCapReached == nil or baseline == true) then
            preyCounter.totalCapReached = totalCapReached
        elseif preyCounter and totalCapReached and preyCounter.totalCapReached ~= true then
            addActivity(activities, "pve_prey", "weekly", "total_cap", L.RENOWN_ACTIVITY_PREY_TOTAL or "Prey total cap reached", 28)
            preyCounter.totalCapReached = true
        elseif preyCounter and not totalCapReached then
            preyCounter.totalCapReached = false
        end
    end

    return true
end

local function collectHousingActivities(activities, view)
    if type(view) ~= "table" or view.available ~= true then
        return false
    end

    for _, task in ipairs(type(view.tasks) == "table" and view.tasks or {}) do
        if task.completed == true then
            addActivity(
                activities,
                "housing_task",
                "weekly",
                task.id or task.title,
                task.title or (L.HOUSING_SUMMARY_TASKS or "Housing Task"),
                SOURCE_DEFAULT_XP.housing_task
            )
        end
    end

    return true
end

local function collectDailyStepActivities(activities, character, snapshot)
    if not ns.GetLiveDailyPveState then
        return false
    end

    local dailyState = ns:GetLiveDailyPveState(character, snapshot and snapshot.weeklyOverview) or {}
    local dailyBountiful = type(dailyState.dailyBountiful) == "table" and dailyState.dailyBountiful or nil
    if not dailyBountiful then
        return false
    end
    if dailyBountiful.renownEligible == false then
        return true
    end
    if ns.DailyPveBountiful
        and type(ns.DailyPveBountiful.IsLevelEligible) == "function"
        and not ns.DailyPveBountiful.IsLevelEligible(character)
    then
        return true
    end

    local completed = tonumber(dailyBountiful.completed) or 0
    local cap = tonumber(dailyBountiful.cap) or 4
    addCountSteps(
        activities,
        "pve_daily",
        "daily",
        "daily_bountiful",
        dailyBountiful.tooltipTitle or (L.PVE_DAILY_ROW_BOUNTIFUL or "Bountiful Delves"),
        completed,
        cap,
        6
    )
    if cap > 0 and completed >= cap then
        addActivity(
            activities,
            "pve_daily",
            "daily",
            "daily_bountiful_complete",
            string.format("%s %d/%d", dailyBountiful.tooltipTitle or (L.PVE_DAILY_ROW_BOUNTIFUL or "Bountiful Delves"), cap, cap),
            16
        )
    end
    return true
end

local function collectMidnightRareActivities(activities, character)
    if not ns.GetLiveMidnightRareState then
        return false
    end

    local state = ns:GetLiveMidnightRareState(character, nil)
    if type(state) ~= "table" or type(state.zones) ~= "table" then
        return false
    end

    local hadData = false
    for _, zone in ipairs(state.zones or {}) do
        for _, rare in ipairs(type(zone) == "table" and zone.rares or {}) do
            if rare and rare.completed == true then
                hadData = true
                addActivity(
                    activities,
                    "pve_rares",
                    "daily",
                    tostring(rare.questID or rare.key or rare.name),
                    string.format(L.RENOWN_ACTIVITY_RARE_KILL or "Rare defeated: %s", rare.name or rare.label or ""),
                    SOURCE_DEFAULT_XP.pve_rares
                )
            end
        end
    end

    return hadData
end

local function collectDelveStepActivities(activities, rows)
    local handled = {}
    for _, row in ipairs(type(rows) == "table" and rows or {}) do
        local entry = row and row.entry
        local key = entry and entry.key
        if key == "delve_bonus_renown" then
            handled[key] = true
            addCountSteps(
                activities,
                "pve_delves",
                "weekly",
                key,
                row.label or entry.label or (L.PVE_DELVES_ROW_BONUS_RENOWN or "Bonus Renown"),
                entry.count,
                entry.maxCount,
                7,
                { accountWide = true }
            )
        elseif isEntryComplete(entry) then
            local rawKey = entry.key or row.id or row.key or row.label
            local amount = SOURCE_DEFAULT_XP.pve_delves
            addActivity(activities, "pve_delves", "weekly", rawKey, row.label or entry.label or rawKey, amount, getRowRenownOptions(row, entry))
        end
    end
    return next(handled) ~= nil
end

local function ensureProfessionKnowledgeCounters(profile)
    profile.counters = type(profile.counters) == "table" and profile.counters or {}
    profile.counters.professionKnowledge = type(profile.counters.professionKnowledge) == "table"
        and profile.counters.professionKnowledge
        or {}
    profile.counters.professionKnowledgeEvents = type(profile.counters.professionKnowledgeEvents) == "table"
        and profile.counters.professionKnowledgeEvents
        or {}
    return profile.counters.professionKnowledge, profile.counters.professionKnowledgeEvents
end

local function collectProfessionKnowledgeSpentActivities(activities, character, profile, baseline)
    if not ns.GetLiveProfessionKnowledgeState or type(profile) ~= "table" then
        return false
    end

    local knowledgeState = ns:GetLiveProfessionKnowledgeState(character, nil)
    local professions = type(knowledgeState) == "table" and type(knowledgeState.professions) == "table" and knowledgeState.professions or nil
    if not professions then
        return false
    end

    local previousByKey, eventIndexByKey = ensureProfessionKnowledgeCounters(profile)
    for index, profession in ipairs(professions) do
        local professionKey = normalizeToken(profession.skillLineID or profession.professionKey or profession.name or index)
        local currentPoints = math.max(0, math.floor(tonumber(profession.points) or 0))
        local previousPoints = tonumber(previousByKey[professionKey])

        if previousPoints == nil or baseline == true then
            previousByKey[professionKey] = currentPoints
        elseif currentPoints < previousPoints then
            local spent = math.min(100, math.floor(previousPoints - currentPoints))
            if spent > 0 then
                local eventIndex = (tonumber(eventIndexByKey[professionKey]) or 0) + 1
                eventIndexByKey[professionKey] = eventIndex
                addActivity(
                    activities,
                    "profession_knowledge",
                    "permanent",
                    "spent_" .. professionKey .. "_" .. tostring(eventIndex),
                    string.format(L.RENOWN_ACTIVITY_PROFESSION_KNOWLEDGE or "Knowledge spent: %s", profession.name or professionKey),
                    spent
                )
            end
            previousByKey[professionKey] = currentPoints
        elseif currentPoints > previousPoints then
            previousByKey[professionKey] = currentPoints
        end
    end

    return true
end

function ns:CollectRenownActivities(character, snapshot, profile, baseline)
    local activities = {}
    local hadData = false
    local pve = ns.UIPveView or {}

    if not snapshot and self.GetLiveVaultSnapshot then
        snapshot = self:GetLiveVaultSnapshot(character)
    end

    hadData = collectVaultActivities(activities, snapshot) or hadData

    local weeklyView = safeBuild(pve.BuildWeeklyListView, character, snapshot)
    if weeklyView then
        hadData = true
        addRows(activities, "pve_weekly", "weekly", weeklyView.rows, SOURCE_DEFAULT_XP.pve_weekly)
    end

    local dailyView = safeBuild(pve.BuildDailyListView, character, snapshot)
    if dailyView then
        hadData = true
        addRows(activities, "pve_daily", "daily", {
            dailyView.rows and dailyView.rows[2],
            dailyView.rows and dailyView.rows[3],
        }, SOURCE_DEFAULT_XP.pve_daily)
        hadData = collectDailyStepActivities(activities, character, snapshot) or hadData
    end

    hadData = collectMidnightRareActivities(activities, character) or hadData

    local eventView = safeBuild(pve.BuildEventsListView, character, snapshot)
    if eventView then
        hadData = true
        addRows(activities, "pve_events", "weekly", eventView.rows, SOURCE_DEFAULT_XP.pve_events)
    end

    local darkmoonView = safeBuild(pve.BuildDarkmoonListView, character, snapshot)
    if darkmoonView and darkmoonView.rows and #darkmoonView.rows > 0 then
        hadData = true
        addRowsWithEntryReset(activities, "pve_darkmoon", darkmoonView.rows, SOURCE_DEFAULT_XP.pve_darkmoon)
    end

    local worldView = safeBuild(pve.BuildWorldListView, character, snapshot)
    if worldView then
        hadData = true
        addRows(activities, "pve_world", "weekly", worldView.rows, SOURCE_DEFAULT_XP.pve_world)
    end

    local delveView = safeBuild(pve.BuildDelvesListView, character, snapshot)
    if delveView then
        hadData = true
        collectDelveStepActivities(activities, delveView.rows)
    end

    local preyView = safeBuild(pve.BuildPreyView, character, snapshot)
    if preyView then
        hadData = collectPreyActivities(activities, preyView, profile, baseline) or hadData
    end

    local pvpView = safeBuild(pve.BuildPvpWeeklyListView, character, snapshot)
    if pvpView then
        hadData = true
        addRows(activities, "pvp_weekly", "weekly", pvpView.rows, SOURCE_DEFAULT_XP.pvp_weekly)
    end

    local professionView = safeBuild(pve.BuildProfessionWeeklyListView, character, snapshot)
    if professionView then
        hadData = true
        addRows(activities, "professions", "weekly", professionView.rows, SOURCE_DEFAULT_XP.professions)
        hadData = collectProfessionKnowledgeSpentActivities(activities, character, profile, baseline) or hadData
    end

    local housingView = ns.UIHousingView and safeBuild(ns.UIHousingView.BuildEndeavorsView, character) or nil
    if housingView then
        hadData = collectHousingActivities(activities, housingView) or hadData
    end

    return activities, hadData
end

local function recordLedger(profile, key, amount, baseline)
    profile.ledger[key] = {
        amount = math.max(0, tonumber(amount) or 0),
        baseline = baseline == true,
        at = getNow(),
    }
end

local function backfillAccountWideLedger(renownDB, activityKey)
    local ledger = renownDB and renownDB.warband and renownDB.warband.ledger
    if type(ledger) ~= "table" or type(activityKey) ~= "string" or activityKey == "" then
        return false
    end

    local legacySuffix = ":" .. activityKey
    for ledgerKey, ledgerEntry in pairs(ledger) do
        if type(ledgerKey) == "string"
            and ledgerKey ~= activityKey
            and ledgerKey:sub(-#legacySuffix) == legacySuffix
        then
            local legacyAmount = 0
            local legacyBaseline = false
            local legacyAt = getNow()
            if type(ledgerEntry) == "table" then
                legacyAmount = tonumber(ledgerEntry.amount) or 0
                legacyBaseline = ledgerEntry.baseline == true
                legacyAt = tonumber(ledgerEntry.at) or legacyAt
            end
            ledger[activityKey] = {
                amount = math.max(0, legacyAmount),
                baseline = legacyBaseline,
                at = legacyAt,
            }
            return true
        end
    end

    return false
end

function ns:GrantRenownActivity(character, activity, baseline)
    if not isRenownSystemEnabled() then
        return false
    end

    if type(activity) ~= "table" or type(activity.key) ~= "string" or activity.key == "" then
        return false
    end

    local renownDB = self:EnsureRenownDB()
    local characterKey = getCharacterKey(character)
    local characterProfile = getCharacterProfile(renownDB, characterKey)
    if not characterProfile then
        return false
    end

    local warbandLedgerKey = (activity.accountWide == true)
        and activity.key
        or (tostring(characterKey) .. ":" .. activity.key)
    if activity.accountWide == true and renownDB.warband.ledger[warbandLedgerKey] then
        return false
    end
    if activity.accountWide == true and backfillAccountWideLedger(renownDB, warbandLedgerKey) then
        return false
    end
    if characterProfile.ledger[activity.key] then
        if activity.accountWide == true then
            local ledgerEntry = characterProfile.ledger[activity.key]
            local legacyAmount = 0
            local legacyBaseline = false
            local legacyAt = getNow()
            if type(ledgerEntry) == "table" then
                legacyAmount = tonumber(ledgerEntry.amount) or 0
                legacyBaseline = ledgerEntry.baseline == true
                legacyAt = tonumber(ledgerEntry.at) or legacyAt
            end
            renownDB.warband.ledger[warbandLedgerKey] = {
                amount = math.max(0, legacyAmount),
                baseline = legacyBaseline,
                at = legacyAt,
            }
        end
        return false
    end

    local amount = math.max(0, math.floor(tonumber(activity.amount) or 0))
    if baseline == true then
        recordLedger(characterProfile, activity.key, 0, true)
        recordLedger(renownDB.warband, warbandLedgerKey, 0, true)
        return false
    end

    local characterLevelBefore = getLevelState(characterProfile.totalXP, "character")
    local warbandLevelBefore = getLevelState(renownDB.warband.totalXP, "warband")
    local characterRankBefore = getRankForLevel(characterLevelBefore, "character")
    local warbandRankBefore = getRankForLevel(warbandLevelBefore, "warband")

    recordLedger(characterProfile, activity.key, amount, false)
    recordLedger(renownDB.warband, warbandLedgerKey, amount, false)
    characterProfile.totalXP = math.max(0, (tonumber(characterProfile.totalXP) or 0) + amount)
    renownDB.warband.totalXP = math.max(0, (tonumber(renownDB.warband.totalXP) or 0) + amount)

    local characterLevelAfter = getLevelState(characterProfile.totalXP, "character")
    local warbandLevelAfter = getLevelState(renownDB.warband.totalXP, "warband")
    local characterRankAfter = getRankForLevel(characterLevelAfter, "character")
    local warbandRankAfter = getRankForLevel(warbandLevelAfter, "warband")

    local entry = {
        at = getNow(),
        characterKey = characterKey,
        label = activity.label or activity.rawKey or activity.key,
        amount = amount,
        source = activity.source or "activity",
        levelBefore = characterLevelBefore.level,
        levelAfter = characterLevelAfter.level,
        paragonBefore = characterLevelBefore.paragon,
        paragonAfter = characterLevelAfter.paragon,
        rankBefore = characterRankBefore and characterRankBefore.label,
        rankAfter = characterRankAfter and characterRankAfter.label,
        warbandLevelBefore = warbandLevelBefore.level,
        warbandLevelAfter = warbandLevelAfter.level,
        warbandParagonBefore = warbandLevelBefore.paragon,
        warbandParagonAfter = warbandLevelAfter.paragon,
        warbandRankBefore = warbandRankBefore and warbandRankBefore.label,
        warbandRankAfter = warbandRankAfter and warbandRankAfter.label,
    }
    pushHistory(characterProfile, entry)
    pushHistory(renownDB.warband, entry)

    if self.UI and self.UI.ShowRenownToast then
        self.UI:ShowRenownToast(entry)
    end
    if self.UI and self.UI.RefreshRenownCard and self.GetSelectedMainTab and self:GetSelectedMainTab() == "chronicle" then
        self.UI:RefreshRenownCard(character)
    end

    return true
end

function ns:ScanRenownProgress(character, snapshot, options)
    if not isRenownSystemEnabled() then
        return 0
    end

    options = type(options) == "table" and options or {}
    local characterKey = getCharacterKey(character)
    if type(characterKey) ~= "string" or characterKey == "" then
        return 0
    end

    if not options.force then
        local now = type(GetTime) == "function" and GetTime() or 0
        local lastScan = RenownRuntime.lastScans[characterKey] or 0
        if now > 0 and lastScan > 0 and (now - lastScan) < SCAN_THROTTLE_SECONDS then
            return 0
        end
        RenownRuntime.lastScans[characterKey] = now
    end

    local renownDB = self:EnsureRenownDB()
    local profile = getCharacterProfile(renownDB, characterKey)
    if not profile then
        return 0
    end

    local schemaBaseline = profile.activitySchemaVersion < CURRENT_RENOWN_ACTIVITY_SCHEMA_VERSION
    local baseline = profile.baselineReady ~= true or schemaBaseline
    local activities, hadData = self:CollectRenownActivities(character, snapshot, profile, baseline)
    local granted = 0
    for _, activity in ipairs(activities or {}) do
        if self:GrantRenownActivity(character, activity, baseline) then
            granted = granted + 1
        end
    end

    if hadData then
        profile.baselineReady = true
        profile.activitySchemaVersion = CURRENT_RENOWN_ACTIVITY_SCHEMA_VERSION
        if type(renownDB.warband) == "table" then
            renownDB.warband.activitySchemaVersion = CURRENT_RENOWN_ACTIVITY_SCHEMA_VERSION
        end
    end

    return granted
end

function ns:GetRenownScope()
    local renownDB = self:EnsureRenownDB()
    if not renownDB or not self.db or not self.db.ui then
        return "character"
    end
    return self.db.ui.renownScope == "warband" and "warband" or "character"
end

function ns:SetRenownScope(scope)
    self:EnsureRenownDB()
    if self.db and self.db.ui then
        self.db.ui.renownScope = scope == "warband" and "warband" or "character"
    end
    if self.UI and self.UI.Refresh then
        self.UI:Refresh()
    end
end

local function getCharacterFrameRank(renownDB, characterKey, frameKey)
    if type(renownDB) ~= "table" or type(characterKey) ~= "string" or characterKey == "" then
        return nil
    end
    if type(frameKey) ~= "string" or frameKey == "" or frameKey == "default" then
        return nil
    end

    local profile = getCharacterProfile(renownDB, characterKey)
    local levelState = getLevelState(profile and profile.totalXP or 0, "character")
    for index, rank in ipairs(RANKS) do
        if rank.key == frameKey and (tonumber(levelState.level) or 1) >= (tonumber(rank.level) or 1) then
            return rank, index
        end
    end
    return nil
end

local function getCharacterBadgeRank(renownDB, characterKey, badgeKey)
    if type(renownDB) ~= "table" or type(characterKey) ~= "string" or characterKey == "" then
        return nil
    end
    if type(badgeKey) ~= "string" or badgeKey == "" or badgeKey == "default" then
        return nil
    end

    local profile = getCharacterProfile(renownDB, characterKey)
    local levelState = getLevelState(profile and profile.totalXP or 0, "character")
    for index, rank in ipairs(RANKS) do
        if rank.key == badgeKey and (tonumber(levelState.level) or 1) >= (tonumber(rank.level) or 1) then
            return rank, index
        end
    end
    return nil
end

function ns:GetRenownPortraitFrameVisible()
    local renownDB = self:EnsureRenownDB()
    return not (renownDB and renownDB.settings and renownDB.settings.showPortraitFrame == false)
end

function ns:SetRenownPortraitFrameVisible(visible)
    local renownDB = self:EnsureRenownDB()
    if not renownDB or not renownDB.settings then
        return false
    end
    renownDB.settings.showPortraitFrame = visible ~= false
    if self.UI and self.UI.Refresh then
        self.UI:Refresh()
    end
    return true
end

function ns:GetRenownSelectedPortraitFrame(character)
    if not isRenownSystemEnabled() then
        return { visible = true, key = "default", index = nil }
    end

    local renownDB = self:EnsureRenownDB()
    if not renownDB or not renownDB.settings then
        return { visible = true, key = "default", index = nil }
    end

    local characterKey = getCharacterKey(character)
    if type(characterKey) ~= "string" or characterKey == "" then
        return { visible = renownDB.settings.showPortraitFrame ~= false, key = "default", index = nil }
    end

    local selectedKey = renownDB.settings.characterFrames[characterKey] or "default"
    local rank, index = getCharacterFrameRank(renownDB, characterKey, selectedKey)
    if selectedKey ~= "default" and not rank then
        selectedKey = "default"
        renownDB.settings.characterFrames[characterKey] = nil
    end

    return {
        visible = renownDB.settings.showPortraitFrame ~= false,
        key = selectedKey,
        index = rank and (rank.assetIndex or index) or nil,
        rank = rank,
    }
end

function ns:SetRenownSelectedPortraitFrame(characterKey, frameKey)
    local renownDB = self:EnsureRenownDB()
    if not renownDB or not renownDB.settings then
        return false
    end

    characterKey = type(characterKey) == "string" and characterKey ~= "" and characterKey or getCharacterKey()
    if type(characterKey) ~= "string" or characterKey == "" then
        return false
    end

    frameKey = type(frameKey) == "string" and frameKey ~= "" and frameKey or "default"
    if frameKey == "default" then
        renownDB.settings.characterFrames[characterKey] = nil
    else
        local rank = getCharacterFrameRank(renownDB, characterKey, frameKey)
        if not rank then
            return false
        end
        renownDB.settings.characterFrames[characterKey] = frameKey
    end

    if self.UI and self.UI.Refresh then
        self.UI:Refresh()
    end
    return true
end

function ns:GetRenownBadgeVisible()
    if not isRenownSystemEnabled() then
        return false
    end

    local renownDB = self:EnsureRenownDB()
    return not (renownDB and renownDB.settings and renownDB.settings.showRenownBadge == false)
end

function ns:SetRenownBadgeVisible(visible)
    local renownDB = self:EnsureRenownDB()
    if not renownDB or not renownDB.settings then
        return false
    end
    renownDB.settings.showRenownBadge = visible ~= false
    if self.UI and self.UI.Refresh then
        self.UI:Refresh()
    end
    return true
end

function ns:GetRenownSelectedBadge(character)
    if not isRenownSystemEnabled() then
        return { visible = false, key = "default", index = nil }
    end

    local renownDB = self:EnsureRenownDB()
    if not renownDB or not renownDB.settings then
        return { visible = true, key = "default", index = nil }
    end

    local characterKey = getCharacterKey(character)
    if type(characterKey) ~= "string" or characterKey == "" then
        return { visible = renownDB.settings.showRenownBadge ~= false, key = "default", index = nil }
    end

    local selectedKey = renownDB.settings.characterBadges[characterKey] or "default"
    local rank, index = getCharacterBadgeRank(renownDB, characterKey, selectedKey)
    if selectedKey ~= "default" and not rank then
        selectedKey = "default"
        renownDB.settings.characterBadges[characterKey] = nil
    end

    return {
        visible = renownDB.settings.showRenownBadge ~= false,
        key = selectedKey,
        index = rank and (rank.assetIndex or index) or nil,
        rank = rank,
    }
end

function ns:SetRenownSelectedBadge(characterKey, badgeKey)
    local renownDB = self:EnsureRenownDB()
    if not renownDB or not renownDB.settings then
        return false
    end

    characterKey = type(characterKey) == "string" and characterKey ~= "" and characterKey or getCharacterKey()
    if type(characterKey) ~= "string" or characterKey == "" then
        return false
    end

    badgeKey = type(badgeKey) == "string" and badgeKey ~= "" and badgeKey or "default"
    if badgeKey == "default" then
        renownDB.settings.characterBadges[characterKey] = nil
    else
        local rank = getCharacterBadgeRank(renownDB, characterKey, badgeKey)
        if not rank then
            return false
        end
        renownDB.settings.characterBadges[characterKey] = badgeKey
    end

    if self.UI and self.UI.Refresh then
        self.UI:Refresh()
    end
    return true
end

function ns:GetRenownWarbandBadgeVisible()
    if not isRenownSystemEnabled() then
        return false
    end

    local renownDB = self:EnsureRenownDB()
    return not (renownDB and renownDB.settings and renownDB.settings.showWarbandBadge == false)
end

function ns:SetRenownWarbandBadgeVisible(visible)
    local renownDB = self:EnsureRenownDB()
    if not renownDB or not renownDB.settings then
        return false
    end

    renownDB.settings.showWarbandBadge = visible ~= false
    if self.UI and self.UI.Refresh then
        self.UI:Refresh()
    end
    return true
end

function ns:GetRenownToastVisible()
    if not isRenownSystemEnabled() then
        return false
    end

    local renownDB = self:EnsureRenownDB()
    return not (renownDB and renownDB.settings and renownDB.settings.showRenownToast == false)
end

function ns:SetRenownToastVisible(visible)
    local renownDB = self:EnsureRenownDB()
    if not renownDB or not renownDB.settings then
        return false
    end

    renownDB.settings.showRenownToast = visible ~= false
    if visible == false and self.UI and self.UI.HideRenownToast then
        self.UI:HideRenownToast()
    end
    if self.UI and self.UI.RefreshRenownCard then
        local character = self.GetSelectedCharacter and self:GetSelectedCharacter() or nil
        self.UI:RefreshRenownCard(character)
    elseif self.UI and self.UI.Refresh then
        self.UI:Refresh()
    end
    return true
end

function ns:GetRenownToastStyle()
    local renownDB = self:EnsureRenownDB()
    local style = renownDB and renownDB.settings and renownDB.settings.toastStyle or nil
    return RENOWN_TOAST_STYLES[style] and style or RENOWN_TOAST_STYLE_DEFAULT
end

function ns:SetRenownToastStyle(style)
    style = RENOWN_TOAST_STYLES[style] and style or RENOWN_TOAST_STYLE_DEFAULT

    local renownDB = self:EnsureRenownDB()
    if not renownDB or not renownDB.settings then
        return false
    end

    renownDB.settings.toastStyle = style
    if self.UI and self.UI.RefreshRenownToastStyle then
        self.UI:RefreshRenownToastStyle()
    end
    if self.UI and self.UI.RefreshRenownCard then
        local character = self.GetSelectedCharacter and self:GetSelectedCharacter() or nil
        self.UI:RefreshRenownCard(character)
    elseif self.UI and self.UI.Refresh then
        self.UI:Refresh()
    end
    return true
end

function ns:GetRenownToastAnimation()
    local renownDB = self:EnsureRenownDB()
    local animation = renownDB and renownDB.settings and renownDB.settings.toastAnimation or nil
    return RENOWN_TOAST_ANIMATIONS[animation] and animation or RENOWN_TOAST_ANIMATION_DEFAULT
end

function ns:SetRenownToastAnimation(animation)
    animation = RENOWN_TOAST_ANIMATIONS[animation] and animation or RENOWN_TOAST_ANIMATION_DEFAULT

    local renownDB = self:EnsureRenownDB()
    if not renownDB or not renownDB.settings then
        return false
    end

    renownDB.settings.toastAnimation = animation
    if self.UI and self.UI.RefreshRenownCard then
        local character = self.GetSelectedCharacter and self:GetSelectedCharacter() or nil
        self.UI:RefreshRenownCard(character)
    elseif self.UI and self.UI.Refresh then
        self.UI:Refresh()
    end
    return true
end

function ns:GetRenownToastScale()
    local renownDB = self:EnsureRenownDB()
    return normalizeRenownToastScale(renownDB and renownDB.settings and renownDB.settings.toastScale or RENOWN_TOAST_SCALE_DEFAULT)
end

function ns:SetRenownToastScale(scale)
    local renownDB = self:EnsureRenownDB()
    if not renownDB or not renownDB.settings then
        return false
    end

    renownDB.settings.toastScale = normalizeRenownToastScale(scale)
    if self.UI and self.UI.RefreshRenownToastStyle then
        self.UI:RefreshRenownToastStyle()
    end
    if self.UI and self.UI.RefreshRenownCard then
        local character = self.GetSelectedCharacter and self:GetSelectedCharacter() or nil
        self.UI:RefreshRenownCard(character)
    elseif self.UI and self.UI.Refresh then
        self.UI:Refresh()
    end
    return true
end

function ns:AdjustRenownToastScale(delta)
    local current = self.GetRenownToastScale and self:GetRenownToastScale() or RENOWN_TOAST_SCALE_DEFAULT
    return self:SetRenownToastScale(current + ((tonumber(delta) or 0) * RENOWN_TOAST_SCALE_STEP))
end

function ns:GetRenownToastPosition()
    local renownDB = self:EnsureRenownDB()
    local position = renownDB and renownDB.settings and renownDB.settings.toastPosition or nil
    if type(position) ~= "table" then
        return {
            point = "CENTER",
            relativePoint = "CENTER",
            x = 0,
            y = 170,
        }
    end

    return {
        point = type(position.point) == "string" and position.point or "CENTER",
        relativePoint = type(position.relativePoint) == "string" and position.relativePoint or "CENTER",
        x = tonumber(position.x) or 0,
        y = tonumber(position.y) or 170,
    }
end

function ns:SetRenownToastPosition(x, y)
    local renownDB = self:EnsureRenownDB()
    if not renownDB or not renownDB.settings then
        return false
    end

    renownDB.settings.toastPosition = {
        point = "CENTER",
        relativePoint = "CENTER",
        x = tonumber(x) or 0,
        y = tonumber(y) or 170,
    }
    return true
end

function ns:ResetRenownToastPosition()
    local renownDB = self:EnsureRenownDB()
    if not renownDB or not renownDB.settings then
        return false
    end

    renownDB.settings.toastPosition = nil
    if self.UI and self.UI.ApplyRenownToastPosition then
        self.UI:ApplyRenownToastPosition()
    end
    return true
end

local function rebuildWarbandProfile(renownDB)
    if type(renownDB) ~= "table" then
        return
    end

    local warband = ensureProfile({})
    for characterKey, profile in pairs(type(renownDB.characters) == "table" and renownDB.characters or {}) do
        profile = ensureProfile(profile)
        warband.totalXP = warband.totalXP + (tonumber(profile.totalXP) or 0)
        for key, ledgerEntry in pairs(profile.ledger or {}) do
            warband.ledger[tostring(characterKey) .. ":" .. tostring(key)] = {
                amount = math.max(0, tonumber(ledgerEntry and ledgerEntry.amount) or 0),
                baseline = ledgerEntry and ledgerEntry.baseline == true,
                at = tonumber(ledgerEntry and ledgerEntry.at) or 0,
            }
        end
        for _, historyEntry in ipairs(profile.history or {}) do
            if type(historyEntry) == "table" then
                local copy = {}
                for key, value in pairs(historyEntry) do
                    copy[key] = value
                end
                warband.history[#warband.history + 1] = copy
            end
        end
    end

    table.sort(warband.history, function(a, b)
        return (tonumber(a and a.at) or 0) > (tonumber(b and b.at) or 0)
    end)
    while #warband.history > HISTORY_LIMIT do
        table.remove(warband.history)
    end
    warband.baselineReady = true
    renownDB.warband = warband
end

function ns:ResetRenownProgress(scope, characterKey)
    local renownDB = self:EnsureRenownDB()
    if not renownDB then
        return false
    end

    if scope == "warband" then
        renownDB.warband = ensureProfile({})
    else
        characterKey = type(characterKey) == "string" and characterKey ~= "" and characterKey or getCharacterKey()
        if type(characterKey) ~= "string" or characterKey == "" then
            return false
        end
        renownDB.characters[characterKey] = ensureProfile({})
        rebuildWarbandProfile(renownDB)
    end

    if self.UI and self.UI.Refresh then
        self.UI:Refresh()
    end
    return true
end

local function buildViewProfile(profile, scope, character)
    profile = ensureProfile(profile)
    local levelState = getLevelState(profile.totalXP, scope)
    local rank = getRankForLevel(levelState, scope)
    local nextRank
    for _, candidate in ipairs(getRanksForScope(scope)) do
        if candidate.level > levelState.level then
            nextRank = {
                label = getRankLabel(candidate, scope),
                level = candidate.level,
                remainingXP = math.max(0, getTotalXPForLevel(candidate.level, scope) - profile.totalXP),
            }
            break
        end
    end

    return {
        scope = scope,
        character = character,
        totalXP = profile.totalXP,
        level = levelState.level,
        paragon = levelState.paragon,
        currentXP = levelState.currentXP,
        neededXP = levelState.neededXP,
        ratio = clamp(levelState.ratio, 0, 1),
        rank = rank,
        nextRank = nextRank,
        history = profile.history or {},
        baselineReady = profile.baselineReady == true,
    }
end

function ns:GetRenownView(character, scope)
    local renownDB = self:EnsureRenownDB()
    scope = scope or self:GetRenownScope()
    if scope == "warband" then
        return buildViewProfile(renownDB and renownDB.warband or {}, "warband", character)
    end

    local characterKey = getCharacterKey(character)
    return buildViewProfile(getCharacterProfile(renownDB, characterKey) or {}, "character", character)
end

Renown.GetLevelState = getLevelState
Renown.GetLevelXP = getLevelXP
Renown.GetRankForLevel = getRankForLevel
Renown.GetRanks = function(scope)
    return getRanksForScope(scope)
end
