-- Dynamic Profiles
-- Named profiles for Edit Mode layouts, UI scale, and AddOn sets.
-- Manual profile selection persists across logins; Auto mode matches by resolution.

local addonName, DP = ...
DP = DP or {}
_G.DynamicProfiles = DP

local DB_VERSION = 5
local defaultPicker = {
    enabled = true,
    point = "CENTER",
    x = 0,
    y = -200,
    width = 110,
    height = 28,
    anchorTarget = "NONE", -- NONE | MINIMAP | CHAT | CUSTOM
    anchorSide = "BOTTOM",  -- LEFT | RIGHT | TOP | BOTTOM
    matchWidth = false,
    offsetX = 0,
    offsetY = -8,
    customFrameName = nil, -- global frame name when anchorTarget == "CUSTOM"
}

local debugLog = {}
local lastAppliedLayoutClean = nil
local lastAppliedProfileID = nil
local reloadBaselineStates = nil
local pendingReloadInfo = nil

-------------------------------------------------------------------------------
-- Utility helpers
-------------------------------------------------------------------------------

local function Debug(msg)
    local timestamp = date("%H:%M:%S")
    table.insert(debugLog, "[" .. timestamp .. "] " .. msg)
    if #debugLog > 50 then table.remove(debugLog, 1) end
end

local function IsChatMuted()
    local db = DynamicProfilesDB
    return db and db.muteChat == true
end

local function Print(msg)
    Debug(msg)
    if IsChatMuted() then return end
    print("|cFF00CCFFDynamic Profiles:|r " .. tostring(msg))
end

DP.Debug = Debug
DP.Print = Print
DP.IsChatMuted = IsChatMuted

local detectedReloadFunction = nil
local function DetectReloadFunction()
    if detectedReloadFunction then return detectedReloadFunction end
    local candidates = {
        C_UI and C_UI.Reload,
        _G.ReloadUI,
        ReloadUI,
        _G.Reload,
    }
    for _, func in ipairs(candidates) do
        if type(func) == "function" then
            detectedReloadFunction = func
            return detectedReloadFunction
        end
    end
    return nil
end

local function StripEscapes(str)
    if not str or type(str) ~= "string" then return "" end
    -- Secret strings (12.0+) still report type() == "string" but error on ops.
    local ok, result = pcall(function()
        local s = str
        s = string.gsub(s, "|c%x%x%x%x%x%x%x%x", "")
        s = string.gsub(s, "|r", "")
        s = string.gsub(s, "|H.-|h(.-)|h", "%1")
        s = string.gsub(s, "|T.-|t", "")
        s = string.gsub(s, "{.-}", "")
        s = string.lower(s)
        return s
    end)
    if not ok then return "" end
    return result
end

local function CleanString(str)
    if not str or type(str) ~= "string" then return "" end
    local ok, result = pcall(function()
        local s = string.gsub(str, "|c%x%x%x%x%x%x%x%x", "")
        s = string.gsub(s, "|r", "")
        s = string.lower(s)
        s = string.gsub(s, "^%s*(.-)%s*$", "%1")
        return s
    end)
    if not ok then return "" end
    return result
end

local function GetCurrentResolution()
    local width, height = GetPhysicalScreenSize()
    if width and height then
        return math.floor(width) .. "x" .. math.floor(height)
    end
    local rawRes = GetCVar("gxWindowedResolution") or GetCVar("gxResolution") or ""
    return rawRes:match("%d+x%d+") or "Unknown"
end

DP.GetCurrentResolution = GetCurrentResolution
DP.CleanString = CleanString

local function DeepCopy(src)
    if type(src) ~= "table" then return src end
    local dst = {}
    for k, v in pairs(src) do
        dst[k] = DeepCopy(v)
    end
    return dst
end

-------------------------------------------------------------------------------
-- SavedVariables + migration
-------------------------------------------------------------------------------

local function MigrateToV5(db)
    -- Legacy profile.addonSet maps → named native sets + addonSetName
    db.addonSets = db.addonSets or {}
    db.protected = db.protected or {}
    db.protected[addonName] = true

    for _, profile in pairs(db.profiles or {}) do
        if type(profile.addonSet) == "table" and next(profile.addonSet) then
            if not profile.addonSetName or profile.addonSetName == "" then
                local base = "Migrated: " .. (profile.name or profile.id or "Profile")
                local setName = base
                local n = 2
                while db.addonSets[setName] do
                    setName = base .. " (" .. n .. ")"
                    n = n + 1
                end
                local members = {}
                if DP.AddonSets and DP.AddonSets.MembersFromLegacyMap then
                    members = DP.AddonSets.MembersFromLegacyMap(profile.addonSet)
                else
                    for name, enabled in pairs(profile.addonSet) do
                        if enabled and type(name) == "string" then
                            table.insert(members, name)
                        end
                    end
                    table.sort(members, function(a, b) return a:lower() < b:lower() end)
                end
                db.addonSets[setName] = members
                profile.addonSetName = setName
            end
            profile.addonSet = nil
        elseif profile.addonSet ~= nil then
            profile.addonSet = nil
        end
        if profile.addonSetName == "" then
            profile.addonSetName = nil
        end
    end
end

local function EnsureDBShape(db)
    local fromVersion = tonumber(db.version) or 0
    db.profiles = db.profiles or {}
    db.autoByResolution = db.autoByResolution or {}
    db.addonSets = db.addonSets or {}
    db.protected = db.protected or {}
    db.protected[addonName] = true
    if db.muteChat == nil then db.muteChat = false end
    db.broker = db.broker or {}
    db.broker.textFormat = db.broker.textFormat or "%profile%"
    db.picker = db.picker or DeepCopy(defaultPicker)
    if db.picker.enabled == nil then db.picker.enabled = true end
    db.picker.point = db.picker.point or defaultPicker.point
    db.picker.x = db.picker.x or defaultPicker.x
    db.picker.y = db.picker.y or defaultPicker.y
    db.picker.width = db.picker.width or defaultPicker.width
    db.picker.height = db.picker.height or defaultPicker.height
    db.picker.anchorTarget = db.picker.anchorTarget or defaultPicker.anchorTarget
    db.picker.anchorSide = db.picker.anchorSide or defaultPicker.anchorSide
    if db.picker.matchWidth == nil then db.picker.matchWidth = defaultPicker.matchWidth end
    db.picker.offsetX = db.picker.offsetX or defaultPicker.offsetX
    db.picker.offsetY = db.picker.offsetY or defaultPicker.offsetY
    -- customFrameName may be nil; keep whatever is saved.
    if db.picker.anchorTarget == "CUSTOM" and (not db.picker.customFrameName or db.picker.customFrameName == "") then
        db.picker.anchorTarget = "NONE"
    end
    db._nextID = db._nextID or 0
    -- activeProfileID = nil means Auto mode

    if fromVersion < 5 then
        MigrateToV5(db)
    end
    db.version = DB_VERSION
    return db
end

local function GetDB()
    DynamicProfilesDB = EnsureDBShape(DynamicProfilesDB or {})
    return DynamicProfilesDB
end

DP.GetDB = GetDB

function DP.SetMuteChat(muted)
    local db = GetDB()
    db.muteChat = muted and true or false
end

local function NewProfileID()
    local db = GetDB()
    db._nextID = (db._nextID or 0) + 1
    return "p" .. db._nextID
end

local function CreateEmptyProfile(overrides)
    local id = NewProfileID()
    local profile = {
        id = id,
        name = "New Profile",
        resolution = GetCurrentResolution(),
        editLayoutBase = nil,
        scale = tonumber(GetCVar("uiScale")) or 1.0,
        suppressChat = false,
        addonSetName = nil, -- named AddOn Set (BAL or native)
    }
    if overrides then
        for k, v in pairs(overrides) do
            profile[k] = v
        end
    end
    profile.id = id
    GetDB().profiles[id] = profile
    return profile
end

DP.CreateEmptyProfile = CreateEmptyProfile

local function DeleteProfile(id)
    local db = GetDB()
    if not db.profiles[id] then return end
    db.profiles[id] = nil
    for res, autoID in pairs(db.autoByResolution) do
        if autoID == id then
            db.autoByResolution[res] = nil
        end
    end
    if db.activeProfileID == id then
        db.activeProfileID = nil
    end
end

DP.DeleteProfile = DeleteProfile

local function GetProfile(id)
    if not id then return nil end
    return GetDB().profiles[id]
end

DP.GetProfile = GetProfile

local function GetSortedProfiles()
    local list = {}
    for _, profile in pairs(GetDB().profiles) do
        table.insert(list, profile)
    end
    table.sort(list, function(a, b)
        return (a.name or ""):lower() < (b.name or ""):lower()
    end)
    return list
end

DP.GetSortedProfiles = GetSortedProfiles

local function SetAutoForResolution(resolution, profileID)
    local db = GetDB()
    if profileID then
        db.autoByResolution[resolution] = profileID
    else
        db.autoByResolution[resolution] = nil
    end
end

DP.SetAutoForResolution = SetAutoForResolution

local function GetAutoProfileID(resolution)
    return GetDB().autoByResolution[resolution or GetCurrentResolution()]
end

DP.GetAutoProfileID = GetAutoProfileID

local function SetActiveProfileID(profileID)
    -- nil = Auto mode
    GetDB().activeProfileID = profileID
end

DP.SetActiveProfileID = SetActiveProfileID

local function GetActiveProfileID()
    return GetDB().activeProfileID
end

DP.GetActiveProfileID = GetActiveProfileID

local function ResolveActiveProfile()
    local db = GetDB()
    local res = GetCurrentResolution()

    if db.activeProfileID then
        local manual = db.profiles[db.activeProfileID]
        if manual then
            return manual, "manual", res
        end
        -- Stale manual id — fall back to Auto.
        db.activeProfileID = nil
    end

    local autoID = db.autoByResolution[res]
    if autoID and db.profiles[autoID] then
        return db.profiles[autoID], "auto", res
    end

    return nil, "none", res
end

DP.ResolveActiveProfile = ResolveActiveProfile

-------------------------------------------------------------------------------
-- Edit Mode / scale / addon apply
-------------------------------------------------------------------------------

local function ApplyEditLayoutInternal(targetNameClean, attemptsLeft, verbose)
    if not EditModeManagerFrame then
        C_AddOns.LoadAddOn("Blizzard_EditMode")
    end

    if C_EditMode and C_EditMode.GetActiveLayoutInfo then
        local activeInfo = C_EditMode.GetActiveLayoutInfo()
        if activeInfo and CleanString(activeInfo.layoutName) == targetNameClean then
            return
        end
    end

    local internalLayouts = EditModeManagerFrame and EditModeManagerFrame:GetLayouts()
    local foundIndex, foundName
    if internalLayouts then
        for i, layoutInfo in ipairs(internalLayouts) do
            if CleanString(layoutInfo.layoutName) == targetNameClean then
                foundIndex = i
                foundName = layoutInfo.layoutName
                break
            end
        end
    end

    if foundIndex then
        if verbose then
            Print("Switched to layout '" .. foundName .. "'.")
        end
        EditModeManagerFrame:SelectLayout(foundIndex)
        lastAppliedLayoutClean = targetNameClean
    elseif attemptsLeft and attemptsLeft > 0 then
        C_Timer.After(1.0, function()
            ApplyEditLayoutInternal(targetNameClean, attemptsLeft - 1, verbose)
        end)
    elseif verbose then
        Print("Could not find Edit Mode layout for '" .. targetNameClean .. "'.")
    end
end

local function ApplyEditLayout(layoutName, verbose)
    if not layoutName or layoutName == "" then return end
    ApplyEditLayoutInternal(CleanString(layoutName), 3, verbose)
end

local function ExecuteReload()
    local reloadFunction = DetectReloadFunction()
    if not reloadFunction then
        Print("ERROR: No reload function available.")
        return
    end
    Print("Reloading UI...")
    local success, err = pcall(reloadFunction)
    if not success then
        Print("FAILED: " .. tostring(err))
    end
end

DP.ExecuteReload = ExecuteReload

local MAX_RELOAD_TOOLTIP_LINES = 12

--- Map addon folder name → { key, title } for real groups (2+ members).
local function BuildAddonGroupLookup()
    local lookup = {}
    if not (DP.AddonSets and DP.AddonSets.GetGroupedAddOns) then
        return lookup
    end
    for _, group in ipairs(DP.AddonSets.GetGroupedAddOns()) do
        if not group.isOther then
            for _, addon in ipairs(group.addons or {}) do
                lookup[addon.name] = { key = group.key, title = group.title or group.key }
            end
        end
    end
    return lookup
end

--- Build tooltip rows for pending enable/disable changes.
--- Fits under the line budget as flat rows when short; otherwise collapses
--- dependency groups (e.g. "BigWigs (+12 dependencies)") so primary packs stay visible.
local function BuildReloadChangeLines(changes)
    local lines = {}
    changes = changes or {}
    if #changes == 0 then return lines end

    local function appendFlat(list)
        for _, change in ipairs(list) do
            table.insert(lines, {
                enabled = change.enabled,
                text = change.title or change.name,
            })
        end
    end

    if #changes <= MAX_RELOAD_TOOLTIP_LINES then
        appendFlat(changes)
        return lines
    end

    local groupOf = BuildAddonGroupLookup()
    -- Buckets keyed by group+direction (or solo name+direction).
    local buckets, order = {}, {}
    for _, change in ipairs(changes) do
        local group = groupOf[change.name]
        local bucketKey
        if group then
            bucketKey = "g:" .. group.key .. ":" .. (change.enabled and "1" or "0")
        else
            bucketKey = "s:" .. change.name .. ":" .. (change.enabled and "1" or "0")
        end
        local bucket = buckets[bucketKey]
        if not bucket then
            bucket = {
                enabled = change.enabled,
                groupTitle = group and group.title or nil,
                groupKey = group and group.key or nil,
                members = {},
            }
            buckets[bucketKey] = bucket
            table.insert(order, bucketKey)
        end
        table.insert(bucket.members, change)
    end

    for _, bucketKey in ipairs(order) do
        local bucket = buckets[bucketKey]
        local members = bucket.members
        if bucket.groupKey and #members >= 2 then
            local parent
            for _, change in ipairs(members) do
                if change.name == bucket.groupKey then
                    parent = change
                    break
                end
            end
            local primary = (parent and parent.title) or bucket.groupTitle or bucket.groupKey
            local extras = parent and (#members - 1) or #members
            local noun = (extras == 1) and "dependency" or "dependencies"
            table.insert(lines, {
                enabled = bucket.enabled,
                text = primary .. " (+" .. extras .. " " .. noun .. ")",
            })
        else
            appendFlat(members)
        end
    end

    table.sort(lines, function(a, b)
        if a.enabled ~= b.enabled then return a.enabled end
        return (a.text or ""):lower() < (b.text or ""):lower()
    end)
    return lines
end

local function AddPendingReloadTooltip(tooltip)
    if not pendingReloadInfo or not tooltip or not tooltip.AddLine then return end

    tooltip:AddLine(" ")
    tooltip:AddLine("|cffff4040* UI reload required|r", 1, 0.25, 0.25)
    if pendingReloadInfo.profileName then
        tooltip:AddLine("Profile: " .. pendingReloadInfo.profileName, 0.85, 0.85, 0.85)
    end
    if pendingReloadInfo.setName then
        tooltip:AddLine("AddOn Set: " .. pendingReloadInfo.setName, 0.85, 0.85, 0.85)
    end

    local displayLines = BuildReloadChangeLines(pendingReloadInfo.changes)
    local shown = 0
    for _, line in ipairs(displayLines) do
        if shown >= MAX_RELOAD_TOOLTIP_LINES then break end
        if line.enabled then
            tooltip:AddLine("+ Enable: " .. line.text, 0.35, 1, 0.45)
        else
            tooltip:AddLine("- Disable: " .. line.text, 1, 0.45, 0.45)
        end
        shown = shown + 1
    end

    local remaining = #displayLines - shown
    if remaining > 0 then
        tooltip:AddLine("... and " .. remaining .. " more", 0.7, 0.7, 0.7)
    end
    tooltip:AddLine("Middle-click to reload now.", 0.9, 0.8, 0.4)
end

function DP.HasPendingReload()
    return pendingReloadInfo ~= nil
end

function DP.GetPendingReloadInfo()
    return pendingReloadInfo
end

function DP.GetReloadIndicator()
    return pendingReloadInfo and " |cffff3030*|r" or ""
end

DP.AddPendingReloadTooltip = AddPendingReloadTooltip

local function ShowReloadPopup(profile)
    if DynamicProfilesReloadPopup then
        if profile and profile.name then
            DynamicProfilesReloadPopupMessage:SetText(
                "AddOn configuration has changed for '" .. profile.name ..
                "'.\nA UI reload is required to apply changes.")
        else
            DynamicProfilesReloadPopupMessage:SetText(
                "AddOn configuration has changed.\nA UI reload is required to apply changes.")
        end
        DynamicProfilesReloadPopup:Show()
    else
        Print("Reload popup unavailable. Type /reload to apply AddOn changes.")
    end
end

local function SnapshotEnableStates()
    local snap = {}
    local character = UnitGUID("player")
    local num = (C_AddOns and C_AddOns.GetNumAddOns and C_AddOns.GetNumAddOns()) or (GetNumAddOns and GetNumAddOns()) or 0
    for i = 1, num do
        local name = (C_AddOns and C_AddOns.GetAddOnInfo and C_AddOns.GetAddOnInfo(i)) or select(1, GetAddOnInfo(i))
        if name then
            local enabled
            if C_AddOns and C_AddOns.GetAddOnEnableState then
                enabled = C_AddOns.GetAddOnEnableState(i, character) > 0
            else
                enabled = not not select(4, GetAddOnInfo(i))
            end
            snap[name] = enabled
        end
    end
    return snap
end

local function GetAddOnTitle(name)
    if C_AddOns and C_AddOns.GetAddOnInfo then
        local _, title = C_AddOns.GetAddOnInfo(name)
        if title and title ~= "" then return title end
    elseif GetAddOnInfo then
        local _, title = GetAddOnInfo(name)
        if title and title ~= "" then return title end
    end
    return name
end

local function BuildEnableStateChanges(before, after)
    local changes = {}
    local seen = {}

    for name, wasEnabled in pairs(before or {}) do
        seen[name] = true
        local isEnabled = after and after[name] or false
        if isEnabled ~= wasEnabled then
            table.insert(changes, {
                name = name,
                title = GetAddOnTitle(name),
                enabled = isEnabled,
            })
        end
    end

    for name, isEnabled in pairs(after or {}) do
        if not seen[name] and isEnabled then
            table.insert(changes, {
                name = name,
                title = GetAddOnTitle(name),
                enabled = true,
            })
        end
    end

    table.sort(changes, function(a, b)
        if a.enabled ~= b.enabled then return a.enabled end
        return a.title:lower() < b.title:lower()
    end)
    return changes
end

local function ApplyAddonSet(profile, verbose)
    if not profile then return end
    local setName = profile.addonSetName
    if not setName or setName == "" then return end
    if not DP.AddonSets or not DP.AddonSets.Apply then return end

    local before = SnapshotEnableStates()
    -- "load": the set is authoritative — enable its members and disable everything
    -- else that isn't protected. This makes an unchecked addon actually turn off.
    DP.AddonSets.Apply(setName, "load", verbose)
    local after = SnapshotEnableStates()
    local immediateChanges = BuildEnableStateChanges(before, after)

    if #immediateChanges > 0 then
        -- Preserve the state that was active when the first pending change was
        -- made. Later profile switches are compared to this baseline so changes
        -- that are switched back before /reload disappear from the warning.
        reloadBaselineStates = reloadBaselineStates or before
        local netChanges = BuildEnableStateChanges(reloadBaselineStates, after)

        if #netChanges > 0 then
            pendingReloadInfo = {
                profileName = profile.name or profile.id,
                setName = setName,
                changes = netChanges,
            }
            ShowReloadPopup(profile)
            if verbose then
                Print("AddOn set '" .. setName .. "' applied. Please reload UI to finish.")
            end
        else
            reloadBaselineStates = nil
            pendingReloadInfo = nil
            if verbose then
                Print("AddOn configuration matches the currently loaded state; reload no longer needed.")
            end
        end
    elseif verbose then
        Print("AddOn set '" .. setName .. "' already matches.")
    end
end

local function ApplyScale(profile, verbose)
    if not profile or not profile.scale then return end

    local currentScale = tonumber(GetCVar("uiScale"))
    local targetScale = tonumber(profile.scale)
    if not currentScale or not targetScale then return end

    if math.abs(currentScale - targetScale) > 0.001 then
        SetCVar("useUiScale", "1")
        SetCVar("uiScale", tostring(profile.scale))
    end
end

local function EvaluateProfileState(verbose)
    local profile, mode, res = ResolveActiveProfile()
    if not profile then
        if verbose then
            Print("No profile for resolution " .. res .. " (Auto mode).")
        end
        if DP.UpdatePickerButton then DP.UpdatePickerButton() end
        return
    end

    if verbose then
        local modeLabel = (mode == "manual") and "manual" or "auto"
        Print("Applying profile '" .. (profile.name or profile.id) .. "' (" .. modeLabel .. ", " .. res .. ").")
    end

    local layoutToUse = profile.editLayoutBase

    ApplyScale(profile, verbose)

    if layoutToUse and layoutToUse ~= "" then
        local layoutClean = CleanString(layoutToUse)
        if layoutClean ~= "" and layoutClean ~= lastAppliedLayoutClean then
            ApplyEditLayout(layoutToUse, verbose)
        end
    end

    -- Only re-check addon sets when the resolved profile identity changes.
    if profile.id ~= lastAppliedProfileID then
        ApplyAddonSet(profile, verbose)
        lastAppliedProfileID = profile.id
    end

    if DP.UpdatePickerButton then DP.UpdatePickerButton() end
end

DP.EvaluateProfileState = EvaluateProfileState

local function SelectProfile(profileID, verbose)
    if profileID ~= nil and not GetProfile(profileID) then
        Print("Unknown profile.")
        return
    end
    lastAppliedProfileID = nil -- force addon re-eval for the new profile
    SetActiveProfileID(profileID)
    EvaluateProfileState(verbose ~= false)
end

DP.SelectProfile = SelectProfile

local function SelectAutoMode(verbose)
    SelectProfile(nil, verbose)
end

DP.SelectAutoMode = SelectAutoMode

-------------------------------------------------------------------------------
-- Chat suppression (installed after SavedVariables are ready)
-------------------------------------------------------------------------------

local chatHookInstalled = false
local function InstallChatHook()
    if chatHookInstalled then return end
    local originalAddMessage = ChatFrame1 and ChatFrame1.AddMessage
    if not originalAddMessage then return end
    chatHookInstalled = true
    ChatFrame1.AddMessage = function(self, text, ...)
        local profile = select(1, ResolveActiveProfile())
        if profile and profile.suppressChat and text then
            local cleanText = StripEscapes(text)
            local formatStr = ERR_EDIT_MODE_LAYOUT_APPLIED
            if formatStr then
                local cleanFormat = StripEscapes(formatStr)
                local prefix = strsplit("%", cleanFormat)
                if prefix and prefix ~= "" and string.find(cleanText, prefix, 1, true) then
                    return
                end
            end
            if string.find(cleanText, "edit mode layout", 1, true) then
                return
            end
        end
        return originalAddMessage(self, text, ...)
    end
end

-------------------------------------------------------------------------------
-- Events
-------------------------------------------------------------------------------

local eventFrame = CreateFrame("Frame")
eventFrame:RegisterEvent("ADDON_LOADED")
eventFrame:RegisterEvent("PLAYER_LOGIN")

eventFrame:SetScript("OnEvent", function(_, event, arg1)
    if event == "ADDON_LOADED" and arg1 == addonName then
        GetDB()
        if DP.AddonSets and DP.AddonSets.Init then DP.AddonSets.Init() end
        InstallChatHook()
        Debug("Dynamic Profiles loaded.")
        if DP.InitPicker then
            C_Timer.After(0.5, DP.InitPicker)
        end
    elseif event == "PLAYER_LOGIN" then
        if DP.AddonSets and DP.AddonSets.Init then DP.AddonSets.Init() end
        if DP.InitAddonListSets then
            C_Timer.After(0.5, DP.InitAddonListSets)
        end
        -- Automatic matching happens once per login/reload. Manual picker
        -- selections still apply immediately when explicitly chosen.
        C_Timer.After(4.0, function()
            EvaluateProfileState(true)
        end)
    end
end)

-------------------------------------------------------------------------------
-- Slash commands
-------------------------------------------------------------------------------

SLASH_DYNAMICPROFILES1 = "/dynamicprofiles"
SLASH_DYNAMICPROFILES2 = "/dp"

SlashCmdList["DYNAMICPROFILES"] = function(msg)
    msg = msg and msg:lower():gsub("^%s+", ""):gsub("%s+$", "") or ""
    if msg == "debug" then
        local profile, mode, res = ResolveActiveProfile()
        Print("Resolution: " .. res)
        Print("Mode: " .. mode)
        Print("Active: " .. (profile and (profile.name .. " [" .. profile.id .. "]") or "none"))
        for _, line in ipairs(debugLog) do
            print("|cff888888" .. line .. "|r")
        end
        return
    elseif msg == "auto" then
        SelectAutoMode(true)
        return
    elseif msg:match("^use%s+") then
        local want = msg:match("^use%s+(.+)$")
        want = want and want:lower()
        for _, profile in ipairs(GetSortedProfiles()) do
            if (profile.name or ""):lower() == want or profile.id:lower() == want then
                SelectProfile(profile.id, true)
                return
            end
        end
        Print("No profile named '" .. tostring(want) .. "'.")
        return
    end

    if DP.OpenOptionsPanel then
        DP.OpenOptionsPanel()
    else
        Print("Options panel not available.")
    end
end
