local addonName, ns = ...
if not ns then return end

-- To add a new module to export/import:
--
--   Simple (BrightQoL subtable):
--     ns.SettingsIO:RegisterSimple("key", "Display Name")
--
--   Custom getter/setter:
--     ns.SettingsIO:Register("key", "Display Name", getterFn, setterFn)
--
-- The UI, serializer, and base64 layer handle everything else automatically.

ns.SettingsIO = { modules = {} }

-- Type schemas for import validation
-- Only defines expected types; missing fields are OK (will get defaults)
local TYPE_SCHEMAS = {
    combatTimer = {
        enabled = "boolean", unlock = "boolean", font = "string",
        colorR = "number", colorG = "number", colorB = "number",
        point = "string", x = "number", y = "number",
        width = "number", height = "number", hidePrefix = "boolean",
        instanceOnly = "boolean", chatReport = "boolean", stickyTimer = "boolean",
        showBackground = "boolean",
    },
    combatAlert = {
        enabled = "boolean", unlock = "boolean", font = "string",
        enterR = "number", enterG = "number", enterB = "number",
        leaveR = "number", leaveG = "number", leaveB = "number",
        point = "string", x = "number", y = "number",
        width = "number", height = "number",
        enterText = "string", leaveText = "string",
    },
    crosshair = {
        enabled = "boolean", size = "number", thickness = "number",
        gap = "number", colorR = "number", colorG = "number", colorB = "number", useClassColor = "boolean",
        opacity = "number", offsetX = "number", offsetY = "number",
        combatOnly = "boolean", dotEnabled = "boolean", dotSize = "number",
        outlineEnabled = "boolean", outlineWeight = "number",
        outlineR = "number", outlineG = "number", outlineB = "number",
        rotation = "number", showTop = "boolean", showRight = "boolean",
        showBottom = "boolean", showLeft = "boolean",
        hideWhileMounted = "boolean",
        dualColor = "boolean", color2R = "number", color2G = "number", color2B = "number",
        circleEnabled = "boolean", circleSize = "number",
        circleR = "number", circleG = "number", circleB = "number",
        meleeRecolor = "boolean", meleeRecolorBorder = "boolean",
        meleeRecolorArms = "boolean", meleeRecolorDot = "boolean", meleeRecolorCircle = "boolean",
        meleeOutColorR = "number", meleeOutColorG = "number", meleeOutColorB = "number",
        meleeSoundEnabled = "boolean", meleeSoundID = "number", meleeSoundInterval = "number",
    },
    combatLogger = {
        enabled = "boolean", instances = "table",
    },
    mouseRing = "table", -- Mouse ring settings
    misc = {
        autoFillDelete = "boolean", fasterLoot = "boolean",
        suppressLootWarnings = "boolean", hideAlerts = "boolean",
        hideTalkingHead = "boolean", hideEventToasts = "boolean",
        hideZoneText = "boolean", autoRepair = "boolean", guildRepair = "boolean",
        durabilityWarning = "boolean", durabilityThreshold = "number",
        autoSlotKeystone = "boolean", skipQueueConfirm = "boolean",
    },
    gcdTracker = {
        enabled = "boolean", unlock = "boolean", duration = "number",
        iconSize = "number", direction = "string", spacing = "number",
        fadeStart = "number", point = "string", x = "number", y = "number",
        combatOnly = "boolean", blocklist = "table", stackOverlapping = "boolean",
        showInDungeon = "boolean", showInRaid = "boolean", showInArena = "boolean",
        showInBattleground = "boolean", showInWorld = "boolean",
        timelineColorR = "number", timelineColorG = "number", timelineColorB = "number",
        timelineHeight = "number",
    },
    stealthReminder = {
        enabled = "boolean", unlock = "boolean", font = "string",
        stealthR = "number", stealthG = "number", stealthB = "number",
        warningR = "number", warningG = "number", warningB = "number",
        stealthText = "string", warningText = "string",
        point = "string", x = "number", y = "number",
        width = "number", height = "number",
        showStealthed = "boolean", showNotStealthed = "boolean",
        stanceEnabled = "boolean", stanceUnlock = "boolean",
        stanceWarnR = "number", stanceWarnG = "number", stanceWarnB = "number",
        stancePoint = "string", stanceX = "number", stanceY = "number",
        stanceWidth = "number", stanceHeight = "number",
        stanceCombatOnly = "boolean", stanceSoundEnabled = "boolean",
        stanceSoundID = "number", stanceSoundInterval = "number",
        stanceWarnText = "string",
    },
    rangeCheck = {
        enabled = "boolean",
        rangeEnabled = "boolean", rangeUnlock = "boolean",
        rangeFont = "string",
        rangeColorR = "number", rangeColorG = "number", rangeColorB = "number",
        rangePoint = "string", rangeX = "number", rangeY = "number",
        rangeWidth = "number", rangeHeight = "number",
        rangeCombatOnly = "boolean",
    },
    focusCastBar = {
        enabled = "boolean", unlock = "boolean",
        point = "string", x = "number", y = "number",
        width = "number", height = "number",
        barColorR = "number", barColorG = "number", barColorB = "number",
        barColorCdR = "number", barColorCdG = "number", barColorCdB = "number",
        bgColorR = "number", bgColorG = "number", bgColorB = "number",
        bgAlpha = "number",
        showIcon = "boolean", iconSize = "number", iconPosition = "string",
        showSpellName = "boolean", showTimeRemaining = "boolean", showInterruptTick = "boolean",
        tickColorR = "number", tickColorG = "number", tickColorB = "number", tickColorUseClassColor = "boolean",
        font = "string", fontSize = "number",
        textColorR = "number", textColorG = "number", textColorB = "number",
        hideFriendlyCasts = "boolean", showEmpowerStages = "boolean",
        showShieldIcon = "boolean", colorNonInterrupt = "boolean",
        nonIntColorR = "number", nonIntColorG = "number", nonIntColorB = "number",
        soundEnabled = "boolean", soundID = "number",
        ttsEnabled = "boolean", ttsMessage = "string", ttsVolume = "number",
    },
    slashCommands = {
        enabled = "boolean",
        commands = "table",
    },
    cRez = {
        enabled = "boolean", unlock = "boolean",
        point = "string", x = "number", y = "number", iconSize = "number",
        timerFontSize = "number", timerColorR = "number", timerColorG = "number", timerColorB = "number", timerAlpha = "number",
        countFontSize = "number", countColorR = "number", countColorG = "number", countColorB = "number", countAlpha = "number",
        deathWarning = "boolean",
    },
    petTracker = {
        enabled = "boolean", unlock = "boolean",
        showIcon = "boolean", onlyInInstance = "boolean",
        point = "string", x = "number", y = "number",
        width = "number", height = "number",
        textSize = "number", iconSize = "number", font = "string",
        missingText = "string", passiveText = "string", wrongPetText = "string",
        colorR = "number", colorG = "number", colorB = "number",
    },
    buffWatcherV2 = {
        enabled = "boolean",
        userEntries = "table",
        categoryEnabled = "table",
        thresholds = "table",
        talentMods = "table",
        disabledDefaults = "table",
        consumableGroupEnabled = "table",
        consumableAutoUse = "table",
        inventoryGroupEnabled = "table",
        classBuffs = "table",
        lastSection = "string",
        reportCardPosition = "table",
    },
    coTank = {
        enabled = "boolean", unlock = "boolean",
        point = "string", x = "number", y = "number",
        width = "number", height = "number", bgAlpha = "number",
        useClassColor = "boolean",
        healthColorR = "number", healthColorG = "number", healthColorB = "number",
        showName = "boolean", nameFormat = "string", nameLength = "number",
        nameFontSize = "number", nameColorUseClassColor = "boolean",
        nameColorR = "number", nameColorG = "number", nameColorB = "number",
    },
}

-- Validate imported data against schema
local function ValidateImportData(key, data)
    if type(data) ~= "table" then
        return false, "expected table"
    end

    local schema = TYPE_SCHEMAS[key]
    if not schema then
        return true -- No schema defined, allow import
    end

    -- Simple table schema (just validate it's a table)
    if schema == "table" then
        return true
    end

    -- Detailed field validation
    for field, expectedType in pairs(schema) do
        local value = data[field]
        if value ~= nil then
            local actualType = type(value)
            if actualType ~= expectedType then
                return false, field .. " should be " .. expectedType .. ", got " .. actualType
            end
        end
    end

    return true
end

function ns.SettingsIO:Register(key, label, getter, setter)
    self.modules[#self.modules + 1] = { key = key, label = label, get = getter, set = setter }
end

function ns.SettingsIO:RegisterSimple(key, label)
    self:Register(key, label,
        function() return BrightQoL[key] end,
        function(d) BrightQoL[key] = d end)
end

local function SerializeValue(v)
    local t = type(v)
    if t == "string" then
        return string.format("%q", v)
    elseif t == "number" then
        return tostring(v)
    elseif t == "boolean" then
        return v and "true" or "false"
    elseif t == "table" then
        local parts = {}
        local n = #v
        for i = 1, n do
            parts[#parts + 1] = SerializeValue(v[i])
        end
        for k, val in pairs(v) do
            if type(k) == "number" and k >= 1 and k <= n and k == math.floor(k) then
                -- already handled in array part
            else
                local kStr
                if type(k) == "string" and k:match("^[%a_][%w_]*$") then
                    kStr = k
                else
                    kStr = "[" .. SerializeValue(k) .. "]"
                end
                parts[#parts + 1] = kStr .. "=" .. SerializeValue(val)
            end
        end
        return "{" .. table.concat(parts, ",") .. "}"
    else
        return "nil"
    end
end

local function Serialize(tbl)
    return SerializeValue(tbl)
end

-- recursive parser (avoids loadstring for security)
local function Deserialize(str)
    if type(str) ~= "string" or str == "" then return nil end
    local pos = 1
    local len = #str
    local MAX_DEPTH = 20

    local parseValue  -- forward declaration

    local function skipWS()
        while pos <= len do
            local b = str:byte(pos)
            if b == 32 or b == 9 or b == 10 or b == 13 then pos = pos + 1
            else break end
        end
    end

    local function peek()
        skipWS()
        return pos <= len and str:byte(pos) or 0
    end

    local function parseString()
        pos = pos + 1  -- skip opening "
        local parts = {}
        while pos <= len do
            local b = str:byte(pos)
            if b == 34 then  -- closing "
                pos = pos + 1
                return table.concat(parts), true
            elseif b == 92 then  -- backslash
                pos = pos + 1
                if pos > len then return nil, false end
                local esc = str:byte(pos)
                if     esc == 110 then parts[#parts + 1] = "\n"
                elseif esc == 116 then parts[#parts + 1] = "\t"
                elseif esc == 114 then parts[#parts + 1] = "\r"
                elseif esc == 92  then parts[#parts + 1] = "\\"
                elseif esc == 34  then parts[#parts + 1] = "\""
                elseif esc == 10  then parts[#parts + 1] = "\n"
                elseif esc >= 48 and esc <= 57 then
                    local numStr = string.char(esc)
                    for _ = 1, 2 do
                        local nb = pos + 1 <= len and str:byte(pos + 1)
                        if nb and nb >= 48 and nb <= 57 then
                            numStr = numStr .. string.char(nb)
                            pos = pos + 1
                        else break end
                    end
                    local code = tonumber(numStr)
                    if not code or code > 255 then return nil, false end
                    parts[#parts + 1] = string.char(code)
                else
                    parts[#parts + 1] = string.char(esc)
                end
                pos = pos + 1
            else
                parts[#parts + 1] = string.char(b)
                pos = pos + 1
            end
        end
        return nil, false  -- unterminated string
    end

    local function parseNumber()
        local start = pos
        if str:byte(pos) == 45 then pos = pos + 1 end  -- minus
        if pos > len or str:byte(pos) < 48 or str:byte(pos) > 57 then
            pos = start; return nil, false
        end
        while pos <= len and str:byte(pos) >= 48 and str:byte(pos) <= 57 do pos = pos + 1 end
        if pos <= len and str:byte(pos) == 46 then  -- decimal
            pos = pos + 1
            while pos <= len and str:byte(pos) >= 48 and str:byte(pos) <= 57 do pos = pos + 1 end
        end
        if pos <= len and (str:byte(pos) == 101 or str:byte(pos) == 69) then  -- exponent
            pos = pos + 1
            if pos <= len and (str:byte(pos) == 43 or str:byte(pos) == 45) then pos = pos + 1 end
            while pos <= len and str:byte(pos) >= 48 and str:byte(pos) <= 57 do pos = pos + 1 end
        end
        local n = tonumber(str:sub(start, pos - 1))
        if not n then pos = start; return nil, false end
        return n, true
    end

    local function isWordChar(b)
        return (b >= 65 and b <= 90) or (b >= 97 and b <= 122)
            or (b >= 48 and b <= 57) or b == 95
    end

    local function parseTable(depth)
        if depth > MAX_DEPTH then return nil, false end
        pos = pos + 1  -- skip {
        local tbl = {}
        local arrayIdx = 1

        while true do
            local b = peek()
            if b == 125 then  -- }
                pos = pos + 1
                return tbl, true
            elseif b == 0 then
                return nil, false
            end

            if b == 91 then  -- [key]=value
                pos = pos + 1
                local key, ok = parseValue(depth + 1)
                if not ok then return nil, false end
                if peek() ~= 93 then return nil, false end  -- ]
                pos = pos + 1
                if peek() ~= 61 then return nil, false end  -- =
                pos = pos + 1
                local val, vok = parseValue(depth + 1)
                if not vok then return nil, false end
                tbl[key] = val
            elseif (b >= 65 and b <= 90) or (b >= 97 and b <= 122) or b == 95 then
                -- identifier or keyword (true/false)
                local idStart = pos
                while pos <= len and isWordChar(str:byte(pos)) do pos = pos + 1 end
                local ident = str:sub(idStart, pos - 1)
                if peek() == 61 then  -- key=value
                    pos = pos + 1
                    local val, vok = parseValue(depth + 1)
                    if not vok then return nil, false end
                    tbl[ident] = val
                else
                    -- bare keyword value
                    local val
                    if ident == "true" then val = true
                    elseif ident == "false" then val = false
                    else return nil, false end
                    tbl[arrayIdx] = val
                    arrayIdx = arrayIdx + 1
                end
            else
                -- plain value (number, string, nested table)
                local val, ok = parseValue(depth + 1)
                if not ok then return nil, false end
                tbl[arrayIdx] = val
                arrayIdx = arrayIdx + 1
            end

            local nb = peek()
            if nb == 44 then  -- comma
                pos = pos + 1
            elseif nb ~= 125 then  -- must be } or ,
                return nil, false
            end
        end
    end

    parseValue = function(depth)
        local b = peek()
        if b == 123 then return parseTable(depth or 0) end     -- {
        if b == 34  then return parseString() end               -- "
        if b == 45 or (b >= 48 and b <= 57) then                -- number
            return parseNumber()
        end
        -- keywords
        if str:sub(pos, pos + 3) == "true" and not isWordChar(str:byte(pos + 4) or 0) then
            pos = pos + 4; return true, true
        end
        if str:sub(pos, pos + 4) == "false" and not isWordChar(str:byte(pos + 5) or 0) then
            pos = pos + 5; return false, true
        end
        return nil, false
    end

    local ok, result = pcall(function()
        local val, success = parseValue(0)
        if not success then return nil end
        skipWS()
        if pos <= len then return nil end  -- trailing chars
        if type(val) ~= "table" then return nil end
        return val
    end)
    if not ok then return nil end
    return result
end

local B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"

local function Base64Encode(data)
    local out = {}
    local len = #data
    for i = 1, len, 3 do
        local a, b, c = data:byte(i, i + 2)
        b = b or 0; c = c or 0
        local n = a * 65536 + b * 256 + c
        out[#out + 1] = B64:sub(math.floor(n / 262144) % 64 + 1, math.floor(n / 262144) % 64 + 1)
        out[#out + 1] = B64:sub(math.floor(n / 4096) % 64 + 1, math.floor(n / 4096) % 64 + 1)
        out[#out + 1] = (i + 1 <= len) and B64:sub(math.floor(n / 64) % 64 + 1, math.floor(n / 64) % 64 + 1) or "="
        out[#out + 1] = (i + 2 <= len) and B64:sub(n % 64 + 1, n % 64 + 1) or "="
    end
    return table.concat(out)
end

local B64_REV = {}
for i = 1, 64 do B64_REV[B64:byte(i)] = i - 1 end

local function Base64Decode(data)
    if type(data) ~= "string" then return nil end
    data = data:gsub("[^A-Za-z0-9+/=]", "")
    local out = {}
    for i = 1, #data, 4 do
        local a, b, c, d = data:byte(i, i + 3)
        a, b = B64_REV[a] or 0, B64_REV[b] or 0
        c = c and (B64_REV[c] or 0) or 0
        d = d and (B64_REV[d] or 0) or 0
        local n = a * 262144 + b * 4096 + c * 64 + d
        out[#out + 1] = string.char(math.floor(n / 65536) % 256)
        if data:sub(i + 2, i + 2) ~= "=" then
            out[#out + 1] = string.char(math.floor(n / 256) % 256)
        end
        if data:sub(i + 3, i + 3) ~= "=" then
            out[#out + 1] = string.char(n % 256)
        end
    end
    return table.concat(out)
end

function ns.SettingsIO:Export()
    local data = {}
    for _, m in ipairs(self.modules) do
        local val = m.get()
        if val then data[m.key] = val end
    end
    return Base64Encode(Serialize(data))
end

function ns.SettingsIO:Preview(encoded)
    local raw = Base64Decode(encoded)
    local data = Deserialize(raw)
    if not data then return nil end
    local found = {}
    for k in pairs(data) do found[k] = true end
    return found
end

function ns.SettingsIO:Import(encoded, selectedKeys)
    local raw = Base64Decode(encoded)
    local data = Deserialize(raw)
    if not data then return false, "Invalid import string." end

    -- Validate all selected modules before applying any changes
    for _, m in ipairs(self.modules) do
        if selectedKeys[m.key] and data[m.key] then
            local valid, err = ValidateImportData(m.key, data[m.key])
            if not valid then
                return false, "Invalid data for " .. m.label .. ": " .. (err or "unknown error")
            end
        end
    end

    -- Apply validated data
    for _, m in ipairs(self.modules) do
        if selectedKeys[m.key] and data[m.key] then
            m.set(data[m.key])
        end
    end
    -- Trigger refresh callbacks for all modules
    self:TriggerRefreshAll()

    return true
end

-- Refresh callback system for reload-free profile switching
ns.SettingsIO.refreshCallbacks = {}

function ns.SettingsIO:RegisterRefresh(key, callback)
    self.refreshCallbacks[key] = callback
end

function ns.SettingsIO:UnregisterRefresh(key)
    self.refreshCallbacks[key] = nil
end

function ns.SettingsIO:TriggerRefreshAll()
    for key, callback in pairs(self.refreshCallbacks) do
        local ok, err = pcall(callback)
        if not ok then
            print("|cffff0000[BrightQoL]|r Refresh error in " .. key .. ": " .. tostring(err))
        end
    end
end

-- Profile Management (profiles are stored as export strings)
-- BrightQoL.profiles = { ["ProfileName"] = "base64string", ... }
-- BrightQoL.activeProfile = "Default"
-- BrightQoL_Profiles = { ["Realm-Character"] = { ["ProfileName"] = "base64string" }, ... }

function ns.SettingsIO:InitProfiles()
    BrightQoL.profiles = BrightQoL.profiles or {}
    BrightQoL.activeProfile = BrightQoL.activeProfile or "Default"
    BrightQoL_Profiles = BrightQoL_Profiles or {}
    self:RegisterAutoSave()
    self:InitSpecProfiles()
    self:RegisterSpecSwap()
end

function ns.SettingsIO:RegisterAutoSave()
    if self.autoSaveRegistered then return end
    self.autoSaveRegistered = true

    local frame = CreateFrame("Frame")
    frame:RegisterEvent("PLAYER_LOGOUT")
    frame:SetScript("OnEvent", function()
        local activeProfile = BrightQoL.activeProfile or "Default"
        ns.SettingsIO:SaveProfile(activeProfile)
    end)
end

-- Spec-based profile switching
-- Each spec stores: { enabled = bool, profile = "name" }
function ns.SettingsIO:InitSpecProfiles()
    BrightQoL.specProfiles = BrightQoL.specProfiles or {}
end

function ns.SettingsIO:RegisterSpecSwap()
    if self.specSwapRegistered then return end
    self.specSwapRegistered = true

    local lastSwapTime = 0
    local lastSpecIndex = nil

    local frame = CreateFrame("Frame")
    frame:RegisterEvent("PLAYER_SPECIALIZATION_CHANGED")
    frame:SetScript("OnEvent", function(_, event, unit)
        -- Event fires with "player" or nil
        if unit and unit ~= "player" then return end

        local specIndex = GetSpecialization()
        if not specIndex then return end

        -- Debounce: ignore if same spec or within 1 second
        local now = GetTime()
        if specIndex == lastSpecIndex and (now - lastSwapTime) < 1 then
            return
        end
        lastSwapTime = now
        lastSpecIndex = specIndex

        local specData = BrightQoL.specProfiles[specIndex]
        if not specData or not specData.enabled then return end

        local profileName = specData.profile
        if profileName and profileName ~= "" and BrightQoL.profiles[profileName] then
            ns.SettingsIO:LoadProfile(profileName)
            local _, specName = GetSpecializationInfo(specIndex)
            print("|cff00aaff[BrightQoL]|r Profile: " .. (specName or ("Spec " .. specIndex)) .. " -> " .. profileName)
        end
    end)
end

function ns.SettingsIO:GetSpecProfileData(specIndex)
    self:InitSpecProfiles()
    return BrightQoL.specProfiles[specIndex] or { enabled = false, profile = nil }
end

function ns.SettingsIO:SetSpecProfileEnabled(specIndex, enabled)
    self:InitSpecProfiles()
    BrightQoL.specProfiles[specIndex] = BrightQoL.specProfiles[specIndex] or { enabled = false, profile = nil }
    BrightQoL.specProfiles[specIndex].enabled = enabled
end

function ns.SettingsIO:SetSpecProfile(specIndex, profileName)
    self:InitSpecProfiles()
    BrightQoL.specProfiles[specIndex] = BrightQoL.specProfiles[specIndex] or { enabled = false, profile = nil }
    BrightQoL.specProfiles[specIndex].profile = profileName
end

local cachedCharKey = nil

function ns.SettingsIO:GetCharacterKey()
    if cachedCharKey then return cachedCharKey end

    local name = UnitName("player")
    local realm = GetRealmName()

    -- Return nil if not ready yet
    if not name or name == "Unknown" or not realm then
        return nil
    end

    cachedCharKey = realm .. "-" .. name
    return cachedCharKey
end

function ns.SettingsIO:GetProfileList()
    self:InitProfiles()
    local list = {}
    for name in pairs(BrightQoL.profiles) do
        list[#list + 1] = name
    end
    table.sort(list)
    return list
end

function ns.SettingsIO:GetActiveProfile()
    self:InitProfiles()
    local active = BrightQoL.activeProfile

    -- Validate that active profile exists
    if active and BrightQoL.profiles[active] then
        return active
    end

    -- Fall back to first available profile
    local list = self:GetProfileList()
    if #list > 0 then
        BrightQoL.activeProfile = list[1]
        return list[1]
    end

    return "Default"
end

function ns.SettingsIO:SaveProfile(name)
    self:InitProfiles()
    local exportStr = self:Export()
    BrightQoL.profiles[name] = exportStr
    BrightQoL.activeProfile = name

    -- Sync to account-wide registry for cross-character copy
    local charKey = self:GetCharacterKey()
    if charKey then
        BrightQoL_Profiles[charKey] = BrightQoL_Profiles[charKey] or {}
        BrightQoL_Profiles[charKey][name] = exportStr
    end

    return true
end

function ns.SettingsIO:CreateDefaultProfile(name)
    self:InitProfiles()

    -- Reset all modules to their defaults
    if ns.ModuleDefaults then
        for moduleName, defaults in pairs(ns.ModuleDefaults) do
            if BrightQoL[moduleName] then
                -- Wipe existing and copy defaults
                wipe(BrightQoL[moduleName])
                for k, v in pairs(defaults) do
                    BrightQoL[moduleName][k] = v
                end
            end
        end
    end

    -- Reset misc settings
    if BrightQoL.misc then
        wipe(BrightQoL.misc)
    end

    -- Export the reset state
    local exportStr = self:Export()
    BrightQoL.profiles[name] = exportStr
    BrightQoL.activeProfile = name

    -- Sync to account-wide registry for cross-character copy
    local charKey = self:GetCharacterKey()
    if charKey then
        BrightQoL_Profiles[charKey] = BrightQoL_Profiles[charKey] or {}
        BrightQoL_Profiles[charKey][name] = exportStr
    end

    -- Trigger refresh to apply defaults to UI
    self:TriggerRefreshAll()

    return true
end

function ns.SettingsIO:LoadProfile(name)
    self:InitProfiles()
    local exportStr = BrightQoL.profiles[name]
    if not exportStr then return false, "Profile not found" end

    -- Import all modules from the stored string
    local allKeys = {}
    for _, m in ipairs(self.modules) do
        allKeys[m.key] = true
    end

    local ok, err = self:Import(exportStr, allKeys)
    if ok then
        BrightQoL.activeProfile = name

        -- Sync to account-wide registry
        local charKey = self:GetCharacterKey()
        if charKey then
            BrightQoL_Profiles[charKey] = BrightQoL_Profiles[charKey] or {}
            BrightQoL_Profiles[charKey][name] = exportStr
        end
    end
    return ok, err
end

function ns.SettingsIO:DeleteProfile(name)
    self:InitProfiles()
    if not BrightQoL.profiles[name] then return false end

    BrightQoL.profiles[name] = nil

    -- Remove from account registry
    local charKey = self:GetCharacterKey()
    if charKey and BrightQoL_Profiles[charKey] then
        BrightQoL_Profiles[charKey][name] = nil
    end

    -- Switch to another profile if we deleted the active one
    if BrightQoL.activeProfile == name then
        local remaining = self:GetProfileList()
        BrightQoL.activeProfile = remaining[1] or "Default"
    end

    return true
end

function ns.SettingsIO:RenameProfile(oldName, newName)
    self:InitProfiles()
    if not BrightQoL.profiles[oldName] then return false, "not_found" end
    if BrightQoL.profiles[newName] then return false, "exists" end

    BrightQoL.profiles[newName] = BrightQoL.profiles[oldName]
    BrightQoL.profiles[oldName] = nil

    -- Update account registry
    local charKey = self:GetCharacterKey()
    if charKey and BrightQoL_Profiles[charKey] and BrightQoL_Profiles[charKey][oldName] then
        BrightQoL_Profiles[charKey][newName] = BrightQoL_Profiles[charKey][oldName]
        BrightQoL_Profiles[charKey][oldName] = nil
    end

    if BrightQoL.activeProfile == oldName then
        BrightQoL.activeProfile = newName
    end

    return true
end

function ns.SettingsIO:GetOtherCharacters()
    self:InitProfiles()
    local chars = {}
    for key in pairs(BrightQoL_Profiles) do
        chars[#chars + 1] = key
    end
    table.sort(chars)
    return chars
end

function ns.SettingsIO:GetCharacterProfiles(charKey)
    self:InitProfiles()
    local profiles = BrightQoL_Profiles[charKey]
    if not profiles then return {} end

    local list = {}
    for name in pairs(profiles) do
        list[#list + 1] = name
    end
    table.sort(list)
    return list
end

function ns.SettingsIO:CopyFromCharacter(charKey, profileName, saveAsName)
    self:InitProfiles()
    local charProfiles = BrightQoL_Profiles[charKey]
    if not charProfiles then return false, "Character not found" end

    local exportStr = charProfiles[profileName]
    if not exportStr then return false, "Profile not found" end

    -- Import all modules
    local allKeys = {}
    for _, m in ipairs(self.modules) do
        allKeys[m.key] = true
    end

    local ok, err = self:Import(exportStr, allKeys)
    if ok then
        -- Save as new profile
        local newName = saveAsName or profileName
        BrightQoL.profiles[newName] = exportStr
        BrightQoL.activeProfile = newName

        -- Sync to account registry
        local myCharKey = self:GetCharacterKey()
        if myCharKey then
            BrightQoL_Profiles[myCharKey] = BrightQoL_Profiles[myCharKey] or {}
            BrightQoL_Profiles[myCharKey][newName] = exportStr
        end
    end
    return ok, err
end

ns.SettingsIO:RegisterSimple("combatTimer",   "Combat Timer")
ns.SettingsIO:RegisterSimple("combatAlert",   "Combat Alert")
ns.SettingsIO:RegisterSimple("crosshair",     "Crosshair")
ns.SettingsIO:RegisterSimple("combatLogger",  "Combat Logger")
ns.SettingsIO:RegisterSimple("mouseRing",     "Mouse Ring")
ns.SettingsIO:RegisterSimple("misc",          "Misc Toggles")
ns.SettingsIO:RegisterSimple("gcdTracker",       "GCD Tracker")
ns.SettingsIO:RegisterSimple("stealthReminder",  "Stealth Reminder")
ns.SettingsIO:RegisterSimple("rangeCheck",       "Range Check")
ns.SettingsIO:RegisterSimple("focusCastBar",     "Focus Cast Bar")
ns.SettingsIO:RegisterSimple("slashCommands",    "Slash Commands")
ns.SettingsIO:RegisterSimple("cRez",             "Combat Rez")
ns.SettingsIO:RegisterSimple("petTracker",       "Pet Tracker")
ns.SettingsIO:RegisterSimple("buffWatcherV2",    "Buff Watcher V2")
ns.SettingsIO:RegisterSimple("coTank",           "Co-Tank Frame")
ns.SettingsIO:RegisterSimple("healerMana",       "Healer Mana")
ns.SettingsIO:RegisterSimple("swiftStats",       "Stats Display")

-- WagoUI Pack Integration API
-- https://forum.wago.io/t/ui-pack-implementation-guide
-- These functions use the same encoding as the in-game Export/Import button
-- (Base64 + custom serializer), so strings are fully interchangeable.

BrightQoL_API = {
    GetModules = function()
        local out = {}
        for _, m in ipairs(ns.SettingsIO.modules) do
            out[#out + 1] = { key = m.key, label = m.label }
        end
        return out
    end,
    GetVersion = function()
        return C_AddOns.GetAddOnMetadata("BrightQoL", "Version") or "unknown"
    end,
}

---@param profileKey string
---@return string|nil
function BrightQoL_API:ExportProfile(profileKey)
    ns.SettingsIO:InitProfiles()
    local charKey = ns.SettingsIO:GetCharacterKey()
    if not charKey then return nil end
    local profiles = BrightQoL_Profiles and BrightQoL_Profiles[charKey]
    if profiles and profiles[profileKey] then
        return profiles[profileKey]  -- already a base64 export string
    end
    -- If the requested key matches the active profile, export current state
    if profileKey == (BrightQoL.activeProfile or "Default") then
        return ns.SettingsIO:Export()
    end
    return nil
end

---@param profileString string
---@param profileKey string
function BrightQoL_API:ImportProfile(profileString, profileKey)
    ns.SettingsIO:InitProfiles()
    -- Import all modules from the string
    local allKeys = {}
    for _, m in ipairs(ns.SettingsIO.modules) do allKeys[m.key] = true end
    local ok, err = ns.SettingsIO:Import(profileString, allKeys)
    if not ok then return false, err end
    -- Save to the profile store
    BrightQoL.profiles = BrightQoL.profiles or {}
    BrightQoL.profiles[profileKey] = profileString
    BrightQoL.activeProfile = profileKey
    local charKey = ns.SettingsIO:GetCharacterKey()
    if charKey then
        BrightQoL_Profiles = BrightQoL_Profiles or {}
        BrightQoL_Profiles[charKey] = BrightQoL_Profiles[charKey] or {}
        BrightQoL_Profiles[charKey][profileKey] = profileString
    end
    -- Per WagoUI spec: do NOT call ReloadUI here
    return true
end

---@param profileString string
function BrightQoL_API:DecodeProfileString(profileString)
    return ns.SettingsIO:Preview(profileString)
end

---@param profileKey string
function BrightQoL_API:SetProfile(profileKey)
    ns.SettingsIO:LoadProfile(profileKey)
end

function BrightQoL_API:GetProfileKeys()
    local list = {}
    local charKey = ns.SettingsIO:GetCharacterKey()
    if charKey and BrightQoL_Profiles and BrightQoL_Profiles[charKey] then
        for name in pairs(BrightQoL_Profiles[charKey]) do
            list[#list + 1] = name
        end
    end
    -- Also include profiles in current char's BrightQoL.profiles
    if BrightQoL.profiles then
        for name in pairs(BrightQoL.profiles) do
            local seen = false
            for _, n in ipairs(list) do if n == name then seen = true end end
            if not seen then list[#list + 1] = name end
        end
    end
    table.sort(list)
    return list
end

function BrightQoL_API:GetCurrentProfileKey()
    return BrightQoL.activeProfile or "Default"
end

function BrightQoL_API:OpenConfig()
    SlashCmdList["BRIGHTQOL"]()
end

function BrightQoL_API:CloseConfig()
    if ns.MainFrame and ns.MainFrame:IsShown() then
        ns.MainFrame:Hide()
    end
end
