local _, WN = ...

local function Bool(v)
    return v == true or v == 1
end

local function TableCount(t)
    local n = 0
    if type(t) == "table" then
        for _ in pairs(t) do n = n + 1 end
    end
    return n
end

function WN:GetBagReport()
    local report = { total = 0, free = 0, used = 0 }
    if not C_Container or type(C_Container.GetContainerNumSlots) ~= "function" then return report end
    local maxBag = tonumber(NUM_TOTAL_EQUIPPED_BAG_SLOTS) or tonumber(NUM_BAG_SLOTS) or 5
    maxBag = math.max(4, math.min(maxBag, 5))
    for bag = 0, maxBag do
        local slots = self:SafeNumber(self:SafeCall(C_Container.GetContainerNumSlots, bag), 0) or 0
        local free = 0
        if type(C_Container.GetContainerNumFreeSlots) == "function" then
            free = self:SafeNumber(self:SafeCall(C_Container.GetContainerNumFreeSlots, bag), 0) or 0
        end
        report.total = report.total + slots
        report.free = report.free + free
    end
    report.used = math.max(0, report.total - report.free)
    return report
end

function WN:RequestEndgameData(force)
    local now = GetTime and GetTime() or 0
    if force ~= true and self.lastEndgameRequestAt and now - self.lastEndgameRequestAt < 15 then return end
    self.lastEndgameRequestAt = now
    if C_MythicPlus and type(C_MythicPlus.RequestMapInfo) == "function" then self:SafeCall(C_MythicPlus.RequestMapInfo) end
    if C_MythicPlus and type(C_MythicPlus.RequestRewards) == "function" then self:SafeCall(C_MythicPlus.RequestRewards) end
    if type(RequestRaidInfo) == "function" then self:SafeCall(RequestRaidInfo) end
end

function WN:GetMythicPlusReport()
    local report = { maps = {}, runs = {}, affixes = {}, score = self:GetMythicRating(), ownedKey = nil, seasonID = 0 }

    if C_MythicPlus and type(C_MythicPlus.GetCurrentSeason) == "function" then
        report.seasonID = self:SafeNumber(self:SafeCall(C_MythicPlus.GetCurrentSeason), 0) or 0
    end

    if C_MythicPlus and type(C_MythicPlus.GetOwnedKeystoneChallengeMapID) == "function" then
        local mapID = self:SafeNumber(self:SafeCall(C_MythicPlus.GetOwnedKeystoneChallengeMapID), nil)
        local level = nil
        if type(C_MythicPlus.GetOwnedKeystoneLevel) == "function" then level = self:SafeNumber(self:SafeCall(C_MythicPlus.GetOwnedKeystoneLevel), nil) end
        if mapID and mapID > 0 then
            local name = nil
            if C_ChallengeMode and type(C_ChallengeMode.GetMapUIInfo) == "function" then name = self:SafeString(self:SafeCall(C_ChallengeMode.GetMapUIInfo, mapID), nil) end
            report.ownedKey = { mapID = mapID, level = level or 0, name = name or self:T("KEYSTONE") }
        end
    end

    if C_MythicPlus and type(C_MythicPlus.GetCurrentAffixes) == "function" then
        local affixes = self:SafeCall(C_MythicPlus.GetCurrentAffixes)
        if type(affixes) == "table" then
            for _, entry in ipairs(affixes) do
                local id = type(entry) == "table" and self:SafeNumber(entry.id, nil) or self:SafeNumber(entry, nil)
                if id then
                    local name, description, icon = nil, nil, nil
                    if C_ChallengeMode and type(C_ChallengeMode.GetAffixInfo) == "function" then
                        name, description, icon = self:SafeCall(C_ChallengeMode.GetAffixInfo, id)
                    end
                    report.affixes[#report.affixes + 1] = { id = id, name = self:SafeString(name, tostring(id)), description = self:SafeString(description, ""), icon = icon }
                end
            end
        end
    end

    local weeklyBest = {}
    if C_MythicPlus and type(C_MythicPlus.GetRunHistory) == "function" then
        local runs = self:SafeCall(C_MythicPlus.GetRunHistory, false, false, true)
        if type(runs) == "table" then
            for _, run in ipairs(runs) do
                if type(run) == "table" then
                    local mapID = self:SafeNumber(run.mapChallengeModeID, nil)
                    local level = self:SafeNumber(run.level, 0) or 0
                    if mapID then
                        report.runs[#report.runs + 1] = {
                            mapID = mapID, level = level, completed = Bool(run.completed), thisWeek = Bool(run.thisWeek),
                            runScore = self:SafeNumber(run.runScore, 0) or 0, durationSec = self:SafeNumber(run.durationSec, 0) or 0,
                        }
                        if not weeklyBest[mapID] or level > weeklyBest[mapID].level then weeklyBest[mapID] = report.runs[#report.runs] end
                    end
                end
            end
        end
    end

    if C_ChallengeMode and type(C_ChallengeMode.GetMapTable) == "function" and type(C_ChallengeMode.GetMapUIInfo) == "function" then
        local mapIDs = self:SafeCall(C_ChallengeMode.GetMapTable)
        if type(mapIDs) == "table" then
            for _, mapID in ipairs(mapIDs) do
                local name, _, timeLimit, texture, backgroundTexture, journalMapID = self:SafeCall(C_ChallengeMode.GetMapUIInfo, mapID)
                name = self:SafeString(name, nil)
                if name then
                    local timed, overtime = nil, nil
                    if C_MythicPlus and type(C_MythicPlus.GetSeasonBestForMap) == "function" then
                        timed, overtime = self:SafeCall(C_MythicPlus.GetSeasonBestForMap, mapID)
                    end
                    local best = nil
                    if type(timed) == "table" then best = timed elseif type(overtime) == "table" then best = overtime end
                    report.maps[#report.maps + 1] = {
                        id = mapID,
                        name = name,
                        timeLimit = self:SafeNumber(timeLimit, 0) or 0,
                        icon = texture,
                        background = backgroundTexture,
                        journalMapID = journalMapID,
                        bestLevel = type(best) == "table" and (self:SafeNumber(best.level, 0) or 0) or 0,
                        bestScore = type(best) == "table" and (self:SafeNumber(best.dungeonScore, 0) or 0) or 0,
                        bestTimed = type(timed) == "table",
                        weeklyLevel = weeklyBest[mapID] and weeklyBest[mapID].level or 0,
                        weeklyDuration = weeklyBest[mapID] and weeklyBest[mapID].durationSec or 0,
                    }
                end
            end
        end
    end

    table.sort(report.maps, function(a, b)
        if a.weeklyLevel ~= b.weeklyLevel then return a.weeklyLevel > b.weeklyLevel end
        if a.bestLevel ~= b.bestLevel then return a.bestLevel > b.bestLevel end
        return a.name < b.name
    end)
    return report
end

function WN:GetRaidReport()
    local report = { lockouts = {}, totalBosses = 0, killedBosses = 0 }
    if type(GetNumSavedInstances) ~= "function" or type(GetSavedInstanceInfo) ~= "function" then return report end
    local count = self:SafeNumber(self:SafeCall(GetNumSavedInstances), 0) or 0
    for index = 1, count do
        local name, _, reset, difficultyID, locked, extended, _, isRaid, maxPlayers, difficultyName, encounters, completed, _, instanceID = self:SafeCall(GetSavedInstanceInfo, index)
        if isRaid == true and self:SafeString(name, nil) then
            local raid = {
                index = index, name = name, reset = self:SafeNumber(reset, 0) or 0,
                difficultyID = self:SafeNumber(difficultyID, 0) or 0, difficultyName = self:SafeString(difficultyName, ""),
                locked = locked == true, extended = extended == true, maxPlayers = self:SafeNumber(maxPlayers, 0) or 0,
                encounters = self:SafeNumber(encounters, 0) or 0, completed = self:SafeNumber(completed, 0) or 0,
                instanceID = self:SafeNumber(instanceID, 0) or 0, bosses = {},
            }
            for encounterIndex = 1, raid.encounters do
                if type(GetSavedInstanceEncounterInfo) == "function" then
                    local bossName, icon, killed = self:SafeCall(GetSavedInstanceEncounterInfo, index, encounterIndex)
                    if self:SafeString(bossName, nil) then
                        raid.bosses[#raid.bosses + 1] = { name = bossName, icon = icon, killed = killed == true }
                    end
                end
            end
            report.totalBosses = report.totalBosses + raid.encounters
            report.killedBosses = report.killedBosses + raid.completed
            report.lockouts[#report.lockouts + 1] = raid
        end
    end
    table.sort(report.lockouts, function(a, b)
        if a.locked ~= b.locked then return a.locked end
        return a.name < b.name
    end)
    return report
end

function WN:GetDelvesReport()
    local report = { season = 0, activeTier = 0, hasActive = false, rewardEligible = false, hasLair = false, worldProgress = nil }
    if C_DelvesUI then
        if type(C_DelvesUI.GetCurrentDelvesSeasonNumber) == "function" then report.season = self:SafeNumber(self:SafeCall(C_DelvesUI.GetCurrentDelvesSeasonNumber), 0) or 0 end
        if type(C_DelvesUI.GetActiveDelveTier) == "function" then
            local tierInfo = self:SafeCall(C_DelvesUI.GetActiveDelveTier)
            if type(tierInfo) == "table" then report.activeTier = self:SafeNumber(tierInfo.tier, 0) or 0 else report.activeTier = self:SafeNumber(tierInfo, 0) or 0 end
        end
        if type(C_DelvesUI.HasActiveDelve) == "function" then report.hasActive = self:SafeBoolean(self:SafeCall(C_DelvesUI.HasActiveDelve), false) or false end
        if type(C_DelvesUI.IsEligibleForActiveDelveRewards) == "function" then report.rewardEligible = self:SafeBoolean(self:SafeCall(C_DelvesUI.IsEligibleForActiveDelveRewards, "player"), false) or false end
        if type(C_DelvesUI.HasActiveLair) == "function" then report.hasLair = self:SafeBoolean(self:SafeCall(C_DelvesUI.HasActiveLair), false) or false end
    end
    local weekly = self:GetWeeklyProgress()
    report.worldProgress = weekly.groups and weekly.groups.DELVES_WORLD or nil
    return report
end

function WN:GetSmartPlan(data)
    data = data or self:GetOverviewData()
    local plan = {}
    local function Add(priority, key, value, done, action, why, reward, where)
        plan[#plan + 1] = { priority = priority, key = key, label = value, done = done == true, action = action, why = why, reward = reward, where = where }
    end

    local warnings = (data.equipment.missingItems or 0) + (data.equipment.missingEnchants or 0) + (data.equipment.emptySockets or 0)
    if warnings > 0 then Add(100, "gear", self:T("PLAN_GEAR", warnings), false, "gear", self:T("WHY_GEAR"), self:T("REWARD_GEAR"), self:T("WHERE_GEAR")) end
    if data.weekly.vaultAvailable then Add(95, "vault", self:T("PLAN_VAULT_CLAIM"), false, "vault", self:T("WHY_VAULT"), self:T("REWARD_VAULT"), self:T("WHERE_VAULT")) end

    local weeklyDefs = {
        { "MYTHIC_PLUS", "PLAN_MPLUS", "group", "WHERE_MPLUS" }, { "RAID", "PLAN_RAID", "adventure", "WHERE_RAID" },
        { "DELVES_WORLD", "PLAN_WORLD", "adventure", "WHERE_WORLD" }, { "PVP", "PLAN_PVP", "group", "WHERE_PVP" },
    }
    for _, def in ipairs(weeklyDefs) do
        local g = data.weekly.groups[def[1]]
        if g and g.threshold > 0 and g.progress < g.threshold then
            local value = self:T(def[2]) .. "  " .. g.progress .. "/" .. g.threshold
            Add(80 - (#plan * 2), def[1], value, false, def[3], self:T("WHY_WEEKLY"), self:T("REWARD_WEEKLY"), self:T(def[4]))
        end
    end
    if data.professions and #data.professions.professions > 0 then
        Add(40, "profession", self:T("PLAN_PROF"), false, "professions", self:T("WHY_PROF"), self:T("REWARD_PROF"), self:T("WHERE_PROF"))
    end
    if #plan == 0 then Add(10, "free", self:T("PLAN_FREE"), true, nil, self:T("WHY_WEEKLY"), self:T("REWARD_WEEKLY"), self:T("WHERE_WORLD")) end
    table.sort(plan, function(a,b) return a.priority > b.priority end)
    return plan
end

function WN:AddCustomTask(text)
    if not self.db then return end
    text = type(text) == "string" and strtrim(text) or ""
    if text == "" then return end
    self.db.customTasks = self.db.customTasks or {}
    local id = tostring(time()) .. tostring(math.random(100,999))
    self.db.customTasks[#self.db.customTasks + 1] = { id = id, text = text, done = false, created = time() }
    self:RefreshAll()
end

function WN:ToggleCustomTask(id)
    for _, task in ipairs(self.db.customTasks or {}) do
        if task.id == id then task.done = not task.done; break end
    end
    self:RefreshAll()
end

function WN:DeleteCustomTask(id)
    for index, task in ipairs(self.db.customTasks or {}) do
        if task.id == id then table.remove(self.db.customTasks, index); break end
    end
    self:RefreshAll()
end

function WN:SaveCharacterSnapshot(data, includeProgress)
    if not self.db or self.db.settings.autoSnapshot == false then return end
    data = data or self.cachedOverview or self:GetOverviewData()
    local key = self:GetCharacterKey()
    self.db.characters = self.db.characters or {}
    local previous = self.db.characters[key] or {}
    local professions = {}
    for _, p in ipairs(data.professions and data.professions.professions or {}) do professions[#professions + 1] = p.name end
    local warningCount = (data.equipment.missingItems or 0) + (data.equipment.missingEnchants or 0) + (data.equipment.emptySockets or 0)
    local weeklyGroups = data.weekly and data.weekly.groups or {}
    local weeklyMPlus = weeklyGroups.MYTHIC_PLUS or {}
    local weeklyRaid = weeklyGroups.RAID or {}
    local weeklyDelves = weeklyGroups.DELVES_WORLD or {}

    local raidKilled, raidTotal = previous.raidKilled, previous.raidTotal
    local delveProgress, delveThreshold, delveTier = previous.delveProgress, previous.delveThreshold, previous.delveTier
    if includeProgress == true then
        local raid = self:GetRaidReport()
        raidKilled = raid and raid.killedBosses or 0
        raidTotal = raid and raid.totalBosses or 0
        local delves = self:GetDelvesReport()
        local world = delves and delves.worldProgress or nil
        delveProgress = world and world.progress or 0
        delveThreshold = world and world.threshold or 0
        delveTier = delves and delves.activeTier or 0
    end

    self.db.characters[key] = {
        key = key,
        name = data.character.name,
        realm = data.character.realm,
        className = data.character.className,
        classFile = data.character.classFile or previous.classFile or data.character.className,
        factionGroup = data.character.factionGroup or previous.factionGroup or "",
        specName = data.character.specName,
        specIcon = data.character.specIcon or previous.specIcon,
        level = data.character.level,
        itemLevel = data.character.equippedItemLevel,
        mythicRating = data.character.mythicRating,
        money = data.currencies.money,
        vaultDone = data.weekly.completedThresholds,
        vaultTotal = data.weekly.totalThresholds,
        vaultAvailable = data.weekly.vaultAvailable,
        gearScore = data.equipment.score,
        gearWarnings = warningCount,
        professions = professions,
        weeklyMPlusProgress = tonumber(weeklyMPlus.progress) or 0,
        weeklyMPlusThreshold = tonumber(weeklyMPlus.threshold) or 0,
        weeklyRaidProgress = tonumber(weeklyRaid.progress) or 0,
        weeklyRaidThreshold = tonumber(weeklyRaid.threshold) or 0,
        weeklyDelveProgress = tonumber(weeklyDelves.progress) or 0,
        weeklyDelveThreshold = tonumber(weeklyDelves.threshold) or 0,
        raidKilled = raidKilled,
        raidTotal = raidTotal,
        delveProgress = delveProgress,
        delveThreshold = delveThreshold,
        delveTier = delveTier,
        lastSeen = time(),
        weeklyNext = self.db.goalCycles and self.db.goalCycles.weeklyNext or 0,
    }
end

function WN:SetAltSort(mode)
    if not self.db then return end
    local valid = { lastSeen=true, itemLevel=true, mythicRating=true, vault=true }
    self.db.altSort = valid[mode] and mode or "lastSeen"
    if self.sections and self.sections.alts then self:UpdateAltsSection() end
end

function WN:GetCharacterSnapshots(sortMode)
    local result = {}
    local invalidKeys = {}
    local now = time and time() or 0
    for key, entry in pairs(self.db and self.db.characters or {}) do
        if type(entry) == "table" then
            local name = type(entry.name) == "string" and strtrim(entry.name) or ""
            if name == "" or name == "?" then
                invalidKeys[#invalidKeys + 1] = key
            else
                entry.key = key
                entry.staleWeekly = (tonumber(entry.weeklyNext) or 0) > 0 and now >= (tonumber(entry.weeklyNext) or 0)
                result[#result + 1] = entry
            end
        else
            invalidKeys[#invalidKeys + 1] = key
        end
    end
    if self.db and self.db.characters then
        for _, key in ipairs(invalidKeys) do self.db.characters[key] = nil end
    end
    local mode = sortMode or (self.db and self.db.altSort) or "lastSeen"
    local function value(entry)
        if mode == "itemLevel" then return tonumber(entry.itemLevel) or 0 end
        if mode == "mythicRating" then return tonumber(entry.mythicRating) or 0 end
        if mode == "vault" then
            local total = tonumber(entry.vaultTotal) or 0
            local done = tonumber(entry.vaultDone) or 0
            if total > 0 then return (done / total) * 100000 + done end
            return done
        end
        return tonumber(entry.lastSeen) or 0
    end
    table.sort(result, function(a,b)
        local av, bv = value(a), value(b)
        if av ~= bv then return av > bv end
        local als, bls = tonumber(a.lastSeen) or 0, tonumber(b.lastSeen) or 0
        if als ~= bls then return als > bls end
        return string.lower(a.name or "") < string.lower(b.name or "")
    end)
    return result
end

function WN:GetFactionTexture(faction)
    if faction == "Alliance" then return "Interface\\TargetingFrame\\UI-PVP-Alliance" end
    if faction == "Horde" then return "Interface\\TargetingFrame\\UI-PVP-Horde" end
    return "Interface\\TargetingFrame\\UI-PVP-FFA"
end

function WN:GetSnapshotClassColor(entry)
    local classFile = entry and (entry.classFile or entry.className)
    local c = classFile and RAID_CLASS_COLORS and RAID_CLASS_COLORS[classFile]
    if c then return c.r or 1, c.g or 1, c.b or 1 end
    return self.colors.text[1], self.colors.text[2], self.colors.text[3]
end

function WN:FormatGoldCompact(copper)
    local gold = (tonumber(copper) or 0) / 10000
    if gold >= 1000000 then return string.format("%.1fM", gold / 1000000) end
    if gold >= 1000 then return string.format("%.1fK", gold / 1000) end
    return string.format("%.0f", gold)
end

function WN:DeleteCharacterSnapshot(key)
    if not self.db or key == self:GetCharacterKey() then return end
    if self.db.characters then self.db.characters[key] = nil end
    self:RefreshAll()
end

WN.integrationProviders = WN.integrationProviders or {}

function WN:RegisterIntegrationProvider(key, provider)
    if type(key) ~= "string" or key == "" or type(provider) ~= "function" then return false end
    self.integrationProviders[key] = provider
    return true
end

function WN:GetIntegrationData(key)
    if not self.db or self.db.settings.enableIntegrations == false or self.db.settings.safeMode then return nil end
    local provider = self.integrationProviders and self.integrationProviders[key]
    if type(provider) ~= "function" then return nil end
    return self:RunModule("integration_"..key, provider)
end

local integrationDefs = {
    { key = "raiderio", names = {"RaiderIO", "Raider.IO"}, label = "Raider.IO" },
    { key = "details", names = {"Details"}, label = "Details!" },
    { key = "dbm", names = {"DBM-Core"}, label = "Deadly Boss Mods" },
    { key = "bigwigs", names = {"BigWigs"}, label = "BigWigs" },
    { key = "weakauras", names = {"WeakAuras"}, label = "WeakAuras" },
    { key = "tomtom", names = {"TomTom"}, label = "TomTom" },
    { key = "elvui", names = {"ElvUI"}, label = "ElvUI" },
    { key = "ellesmere", names = {"EllesmereUI"}, label = "EllesmereUI" },
    { key = "evui", names = {"EVUI", "EvUI", "evui"}, label = "EvUI" },
}

function WN:GetIntegrationReport()
    local report = {}
    local integrationsEnabled = self.db and self.db.settings.enableIntegrations ~= false and self.db.settings.safeMode ~= true
    local isLoaded = C_AddOns and C_AddOns.IsAddOnLoaded or IsAddOnLoaded
    for _, def in ipairs(integrationDefs) do
        local detected, loadedName = false, nil
        if integrationsEnabled then
            for _, name in ipairs(def.names) do
                if type(isLoaded) == "function" and self:SafeBoolean(self:SafeCall(isLoaded, name), false) then detected = true; loadedName = name; break end
            end
        end
        report[#report + 1] = { key = def.key, label = def.label, detected = detected, addonName = loadedName, provider = self.integrationProviders[def.key] ~= nil, disabled = not integrationsEnabled }
    end
    return report
end

function WN:BuildNotifications(data)
    data = data or self.cachedOverview or self:GetOverviewData()
    local notices = {}
    if data.weekly.vaultAvailable then notices[#notices + 1] = { key = "vault", text = self:T("NOTIFY_VAULT_READY"), action = "vault", severity = 3 } end
    local gearWarnings = (data.equipment.missingItems or 0) + (data.equipment.missingEnchants or 0) + (data.equipment.emptySockets or 0)
    if gearWarnings > 0 then notices[#notices + 1] = { key = "gear", text = self:T("NOTIFY_GEAR_LEFT", gearWarnings), action = "gear", severity = 3 } end
    local left = math.max(0, (data.weekly.totalThresholds or 0) - (data.weekly.completedThresholds or 0))
    if left > 0 then notices[#notices + 1] = { key = "weekly", text = self:T("NOTIFY_WEEKLY_LEFT", left), action = "planner", severity = 2 } end
    local reset = self:GetResetInfo()
    if left > 0 and (reset.weekly or 0) > 0 and reset.weekly <= 86400 then
        notices[#notices + 1] = { key = "reset", text = self:T("NOTIFY_RESET_SOON", self:FormatDuration(reset.weekly)), action = "planner", severity = 3 }
    end
    if #notices == 0 then notices[1] = { key = "clear", text = self:T("NOTIFY_ALL_CLEAR"), severity = 1 } end
    return notices
end

function WN:ShowLoginSummary(data)
    if not self.db or self.db.settings.smartNotifications == false or self.db.settings.notifyLogin == false then return end
    if self.loginSummaryShown then return end
    self.loginSummaryShown = true
    local notices = self:BuildNotifications(data)
    for i = 1, math.min(3, #notices) do self:Print(notices[i].text) end
end

function WN:InitDiagnostics()
    if self.diagnosticsInitialized and self.diagnostics then return end
    local fresh = { errors = {}, blocked = {}, moduleFailures = {}, disabledModules = {} }
    if self.db then
        if type(self.db.diagnostics) ~= "table" then self.db.diagnostics = fresh end
        self.db.diagnostics.errors = self.db.diagnostics.errors or {}
        self.db.diagnostics.blocked = self.db.diagnostics.blocked or {}
        -- Module failure/disable flags are session state. Persisting them caused a fixed
        -- module to remain disabled after an addon update or /reload.
        self.db.diagnostics.moduleFailures = {}
        self.db.diagnostics.disabledModules = {}
        self.diagnostics = self.db.diagnostics
    else
        self.diagnostics = fresh
    end
    self.diagnosticsInitialized = true
end

function WN:RecordError(moduleName, errorText)
    self:InitDiagnostics()
    local d = self.diagnostics
    d.errors[#d.errors + 1] = { module = moduleName or "?", text = tostring(errorText or "?"), time = date("%H:%M:%S") }
    while #d.errors > 25 do table.remove(d.errors, 1) end
    d.moduleFailures[moduleName] = (d.moduleFailures[moduleName] or 0) + 1
    if d.moduleFailures[moduleName] >= 3 then
        d.disabledModules[moduleName] = true
        self:Print(self:T("MODULE_DISABLED", moduleName))
    end
end

function WN:RecordBlockedAction(addonName, functionName)
    if addonName ~= self.name and addonName ~= "WoWNext" then return end
    self:InitDiagnostics()
    local d = self.diagnostics
    d.blocked[#d.blocked + 1] = { addon = addonName, func = tostring(functionName or "?"), time = date("%H:%M:%S") }
    while #d.blocked > 25 do table.remove(d.blocked, 1) end
end

function WN:RunModule(moduleName, func, ...)
    self:InitDiagnostics()
    if self.diagnostics.disabledModules[moduleName] then return nil end
    if type(func) ~= "function" then return nil end
    local args = { ... }
    local results = { xpcall(function() return func(unpack(args)) end, function(err) return tostring(err) end) }
    if not results[1] then
        self:RecordError(moduleName, results[2])
        return nil
    end
    table.remove(results, 1)
    return unpack(results)
end

function WN:ClearDiagnostics()
    self.diagnostics = { errors = {}, blocked = {}, moduleFailures = {}, disabledModules = {} }
    if self.db then self.db.diagnostics = self.diagnostics end
    self:RefreshAll()
end

function WN:SetSafeMode(enabled)
    if not self.db then return end
    self.db.settings.safeMode = enabled == true
    if enabled then
        self.db.safeModeBackup = self.db.safeModeBackup or {
            autoRefresh = self.db.settings.autoRefresh,
            enableIntegrations = self.db.settings.enableIntegrations,
        }
        self.db.settings.autoRefresh = false
        self.db.settings.enableIntegrations = false
        if self.widgets then
            for _, widget in pairs(self.widgets) do widget:Hide() end
        end
        self:Print(self:T("SAFE_MODE_ON"))
    else
        if type(self.db.safeModeBackup) == "table" then
            self.db.settings.autoRefresh = self.db.safeModeBackup.autoRefresh
            self.db.settings.enableIntegrations = self.db.safeModeBackup.enableIntegrations
        end
        self.db.safeModeBackup = nil
        self:Print(self:T("SAFE_MODE_OFF"))
    end
    self:ApplySettings()
    self:RefreshAll()
end

local profileKeys = {
    "language","uiScale","opacity","showTracker","lockTracker","showMinimap","openOnLogin","autoRefresh","checkEnchants","privateTooltips","lockWindow",
    "smartNotifications","notifyLogin","autoSnapshot","showSearch","showFavorites","enableIntegrations","theme","widgetPerformance","widgetCharacter","widgetCurrency","widgetWeekly","widgetBags",
}

function WN:ExportProfileString()
    if not self.db or not self.db.settings then return "WN1|" end
    local parts = {}
    for _, key in ipairs(profileKeys) do
        local value = self.db.settings[key]
        if type(value) == "boolean" then value = value and "1" or "0" end
        if value ~= nil then parts[#parts + 1] = key .. "=" .. tostring(value) end
    end
    local favs = {}
    for action, enabled in pairs(self.db.favorites or {}) do if enabled then favs[#favs + 1] = action end end
    table.sort(favs)
    parts[#parts + 1] = "favorites=" .. table.concat(favs, ",")
    local accent = self.db.settings.customAccent or {0.07,0.82,0.815}
    parts[#parts + 1] = "accent=" .. string.format("%.4f,%.4f,%.4f", tonumber(accent[1]) or 0.07, tonumber(accent[2]) or 0.82, tonumber(accent[3]) or 0.815)

    local posParts = {}
    for key, pos in pairs(self.db.positions or {}) do
        if type(pos) == "table" and pos.point then
            local x = math.floor((tonumber(pos.x) or 0) + 0.5)
            local y = math.floor((tonumber(pos.y) or 0) + 0.5)
            posParts[#posParts + 1] = table.concat({key, pos.point, pos.relativePoint or pos.point, x, y}, ",")
        end
    end
    table.sort(posParts)
    parts[#parts + 1] = "positions=" .. table.concat(posParts, "~")
    return "WN1|" .. table.concat(parts, ";")
end

function WN:ImportProfileString(text)
    if type(text) ~= "string" or string.sub(text, 1, 4) ~= "WN1|" then return false end
    local allowed = {}
    for _, key in ipairs(profileKeys) do allowed[key] = true end
    for token in string.gmatch(string.sub(text, 5), "[^;]+") do
        local key, value = string.match(token, "^([^=]+)=(.*)$")
        if key == "favorites" then
            self.db.favorites = {}
            for fav in string.gmatch(value or "", "[^,]+") do self.db.favorites[fav] = true end
        elseif key == "accent" then
            local r,g,b = string.match(value or "", "^([%d%.]+),([%d%.]+),([%d%.]+)$")
            if r and g and b then self.db.settings.customAccent = {tonumber(r) or 0.07, tonumber(g) or 0.82, tonumber(b) or 0.815} end
        elseif key == "positions" then
            self.db.positions = self.db.positions or {}
            for packed in string.gmatch(value or "", "[^~]+") do
                local id, point, relativePoint, x, y = string.match(packed, "^([^,]+),([^,]+),([^,]+),([^,]+),([^,]+)$")
                if id and point and relativePoint then
                    self.db.positions[id] = { point = point, relativePoint = relativePoint, x = tonumber(x) or 0, y = tonumber(y) or 0 }
                end
            end
        elseif key and allowed[key] then
            if value == "1" then value = true elseif value == "0" then value = false elseif key == "uiScale" or key == "opacity" then value = tonumber(value) or self.db.settings[key] end
            self.db.settings[key] = value
        end
    end
    self:ApplyLocale()
    if self.ApplyTheme then self:ApplyTheme(self.db.settings.theme or "midnight", true) end
    self:ApplySettings()
    if self.RestorePositions then self:RestorePositions() end
    if self.widgets then
        local defaults = { performance={-390,300}, character={-195,300}, currency={0,300}, weekly={195,300}, bags={390,300} }
        for id, xy in pairs(defaults) do self:RestoreWidgetPosition(id, xy[1], xy[2]) end
    end
    self:UpdateStaticTexts()
    self:RefreshAll()
    return true
end

function WN:ToggleFavorite(action, label)
    if not self.db then return end
    self.db.favorites = self.db.favorites or {}
    self.db.favorites[action] = not self.db.favorites[action]
    self:Print(self:T(self.db.favorites[action] and "FAVORITE_ADDED" or "FAVORITE_REMOVED", label or action))
    self:RefreshAll()
end

function WN:IsFavorite(action)
    return self.db and self.db.favorites and self.db.favorites[action] == true
end

function WN:GetAccountWeeklySummary()
    local result = { characters = 0, completed = 0, total = 0, ready = 0, stale = 0, money = 0, raidKilled = 0, raidTotal = 0 }
    for _, entry in ipairs(self:GetCharacterSnapshots()) do
        result.characters = result.characters + 1
        result.money = result.money + (tonumber(entry.money) or 0)
        result.raidKilled = result.raidKilled + (tonumber(entry.raidKilled) or 0)
        result.raidTotal = result.raidTotal + (tonumber(entry.raidTotal) or 0)
        if entry.staleWeekly then
            result.stale = result.stale + 1
        else
            result.completed = result.completed + (tonumber(entry.vaultDone) or 0)
            result.total = result.total + (tonumber(entry.vaultTotal) or 0)
            if entry.vaultAvailable then result.ready = result.ready + 1 end
        end
    end
    return result
end

function WN:GetAdvancedOverviewData()
    return {
        mythic = self:GetMythicPlusReport(),
        raid = self:GetRaidReport(),
        delves = self:GetDelvesReport(),
        bags = self:GetBagReport(),
        integrations = self:GetIntegrationReport(),
        alts = self:GetCharacterSnapshots(),
        account = self:GetAccountWeeklySummary(),
    }
end
