-- Chat/Slash commands (/msuf) + small tooltip helpers + Blizzard Edit Mode bridge
-- Thinking about just scrapping this if this causes more erros
local addonName, ns = ...
ns = ns or {}

local function Tr(text)
    if type(text) ~= "string" then return text end
    if type(ns.Translate) == "function" then return ns.Translate(text) end
    local locale = ns.L or _G.MSUF_L
    if type(locale) == "table" then
        local translated = rawget(locale, text)
        if translated ~= nil then return translated end
    end
    return text
end

ns.Debug = ns.Debug or {}
local Debug = ns.Debug

Debug.IsGFHoverEnabled = Debug.IsGFHoverEnabled or function()
    return Debug.gfHover == true
end

Debug.PrintGFHover = Debug.PrintGFHover or function(message, ...)
    if Debug.gfHover ~= true then return end
    local prefix = "|cff7aa2f7MSUF GFDBG|r "
    if select("#", ...) > 0 then
        local ok, formatted = pcall(string.format, message, ...)
        if ok then
            print(prefix .. formatted)
            return
        end
    end
    print(prefix .. tostring(message))
end
local function MSUF_Chat_RunEnsureDB()
    local ensureDB = _G.MSUF_EnsureDB
    if type(ensureDB) == "function" then
        ensureDB()
        return true
    end
    return false
end
local function MSUF_Chat_RunApplyAllSettings()
    local applyAll = _G.MSUF_ApplyAllSettings
    if type(applyAll) == "function" then
        applyAll()
        return true
    end
    return false
end

local MSUF_RESET_DEFAULTS = {
    player = { width=275, height=40, offsetX=-260, offsetY=80, showName=true, showHP=true, showPower=true },
    target = { width=275, height=40, offsetX= 260, offsetY=80, showName=true, showHP=true, showPower=true },
    focus  = { width=220, height=30, offsetX= 260, offsetY=135, showName=true, showHP=false, showPower=false },
    pet    = { width=220, height=30, offsetX=-260, offsetY=135, showName=true, showHP=false, showPower=false },
    targettarget = { width=220, height=30, offsetX=260, offsetY=225, showName=true, showHP=true, showPower=false },
    focustarget = { width=180, height=30, offsetX=260, offsetY=180, showName=true, showHP=true, showPower=false },
    boss   = { width=180, height=30, offsetX=360, offsetY=230, spacing=-96, bossLayoutMode="VERTICAL_DOWN", showName=true, showHP=true, showPower=false },
}
local MSUF_RESET_ANCHOR_UNITS = { "player", "target", "focus", "focustarget", "pet", "targettarget", "boss" }
local MSUF_FullResetPending = false
local function MSUF_ResetPositionAnchorsToScreen()
    if type(MSUF_DB) ~= "table" then return end

    MSUF_DB.general = MSUF_DB.general or {}
    local g = MSUF_DB.general
    g.anchorToCooldown = false
    g.anchorName = "UIParent"

    for i = 1, #MSUF_RESET_ANCHOR_UNITS do
        local unit = MSUF_RESET_ANCHOR_UNITS[i]
        local conf = MSUF_DB[unit]
        if type(conf) == "table" then
            conf.anchorFrameName = nil
            conf.anchorToUnitframe = "GLOBAL"
        end
    end
end
local function MSUF_DoFullReset(opts)
    opts = opts or {}
    local skipReload = (opts.skipReload == true)
    if InCombatLockdown and InCombatLockdown() then
        print(Tr("|cffff0000MSUF:|r Cannot do FULL reset while in combat."))
         return
    end
    MSUF_DB = nil
    MSUF_GlobalDB = nil
    MSUF_ActiveProfile = nil
    print(Tr("|cffff0000MSUF:|r FULL RESET executed - all MSUF profiles & settings deleted for this account."))
    if skipReload then
        print(Tr("|cffffff00MSUF:|r Reset staged. Please type |cff00ff00/reload|r OR use: MSUF Menu > Advanced > Factory Reset."))
         return
    end
    print(Tr("|cffffff00MSUF:|r Reloading UI to rebuild clean defaults..."))
	-- NOTE: C_UI.Reload() is protected; addons may get ADDON_ACTION_BLOCKED.
	-- ReloadUI() is the safe public API for addons.
	if type(ReloadUI) == "function" then
		ReloadUI()
	end
 end
-- Expose for the Slash Menu (button click = hardware event, safe for ReloadUI)
_G.MSUF_DoFullReset = MSUF_DoFullReset
local function MSUF_PrintHelp()
    print(Tr("|cff00ff00MSUF commands:|r"))
    print(Tr("  /msuf help      - Show this help."))
    print(Tr("  /msuf reset     - Reset all MSUF frame positions and visibility to defaults."))
    print(Tr("  /msuf fullreset - FULL factory reset (all profiles/settings)."))
    print(Tr("                   Confirm stages the reset; reload via /reload or MSUF Menu > Advanced > Factory Reset."))
    print(Tr("  /msuf absorb    - Toggle showing total absorb amount in HP text."))
    print(Tr("  /msuf analytics off|on|status - Toggle Wago Analytics beta telemetry."))
    print(Tr("  /msuf gfhoverdebug on|off|status - Debug group-frame hover + tooltip paths."))
    print(Tr("  /msuf inputdebug - Print keyboard-capture frames for movement-lock diagnosis."))
    print(Tr("  /rl             - Reload the UI."))
    print(Tr("  /msufdbgpos     - Toggle position drift debugger (overlay + chat log)."))
    print(Tr("  !msuf help      - Print this help via chat (from your own character)."))
 end

local function MSUF_FrameDebugName(frame)
    if not frame then return "nil" end
    local name = frame.GetName and frame:GetName()
    if type(name) == "string" and name ~= "" then return name end
    return tostring(frame)
end

local function MSUF_SafeGetKeyboardPropagation(frame)
    if not (frame and frame.GetPropagateKeyboardInput) then return nil end
    local ok, value = pcall(frame.GetPropagateKeyboardInput, frame)
    if not ok then return nil end
    return value
end

local function MSUF_PrintInputDebug()
    print("|cff7aa2f7MSUF INPUT|r keyboard diagnostics")
    print("Bindings: W=" .. tostring(GetBindingAction and GetBindingAction("W") or "?")
        .. " SPACE=" .. tostring(GetBindingAction and GetBindingAction("SPACE") or "?"))

    local focus = GetCurrentKeyBoardFocus and GetCurrentKeyBoardFocus()
    print("Keyboard focus: " .. MSUF_FrameDebugName(focus))

    local st = _G.MSUF_EditState
    print("MSUF edit: active=" .. tostring(st and st.active)
        .. " popupOpen=" .. tostring(st and st.popupOpen)
        .. " unit=" .. tostring(st and st.unitKey))

    if type(EnumerateFrames) ~= "function" then
        print("EnumerateFrames unavailable.")
        return
    end

    local found, scanned = 0, 0
    local frame = EnumerateFrames()
    while frame and scanned < 12000 do
        scanned = scanned + 1
        local keyboard = frame.IsKeyboardEnabled and frame:IsKeyboardEnabled()
        if keyboard then
            local shown = (not frame.IsShown) or frame:IsShown()
            local propagate = MSUF_SafeGetKeyboardPropagation(frame)
            if shown or propagate == false then
                found = found + 1
                if found <= 20 then
                    print(string.format("%02d %s shown=%s propagate=%s strata=%s level=%s",
                        found,
                        MSUF_FrameDebugName(frame),
                        tostring(shown),
                        tostring(propagate),
                        tostring(frame.GetFrameStrata and frame:GetFrameStrata() or "?"),
                        tostring(frame.GetFrameLevel and frame:GetFrameLevel() or "?")))
                end
            end
        end
        frame = EnumerateFrames(frame)
    end
    if found == 0 then
        print("No shown keyboard-enabled frames found.")
    elseif found > 20 then
        print("... " .. tostring(found - 20) .. " more keyboard-enabled frames hidden.")
    end
end
-- Optional chat trigger: "!msuf help" (only from yourself)
-- Midnight/Beta secret-safe: chat event args can become "secret" in combat.
-- Never boolean-test/compare them directly and never call string methods via ':'.
local NotSecretValue = _G.NotSecretValue
local function MSUF__Chat_IsSafeString(v)
    -- Secret-safe: never call string methods on secret strings (Midnight 12.0).
    if type(v) ~= "string" then
        return false
    end

    local isv = _G.issecretvalue
        or (C_Secrets and type(C_Secrets.IsSecret) == "function" and C_Secrets.IsSecret)
        or nil

    if isv and isv(v) then
        return false
    end

    local NotSecretValue = _G.NotSecretValue
    if NotSecretValue then
        return NotSecretValue(v)
    end

    -- If we cannot detect secret values, be conservative in combat.
    if InCombatLockdown and InCombatLockdown() then
        return false
    end

    return true
end
local function MSUF__Chat_IsFromSelf(author, ...)
    -- Prefer GUID-based self-check to avoid comparing author strings.
    local senderGUID = select(10, ...)
    local myGUID = UnitGUID and UnitGUID("player")
    if MSUF__Chat_IsSafeString(senderGUID) and MSUF__Chat_IsSafeString(myGUID) then
        return senderGUID == myGUID
    end
    -- Fallback: compare short author name (strip realm) only if strings are safe.
    local myName = UnitName and UnitName("player")
    if not MSUF__Chat_IsSafeString(myName) or not MSUF__Chat_IsSafeString(author) then
         return false
    end
    local shortAuthor = author
    local dash = string.find(author, "-", 1, true)
    if dash and dash > 1 then
        shortAuthor = string.sub(author, 1, dash - 1)
    end
    return (shortAuthor == myName)
end
local function MSUF__Chat_GetLowerTrimmed(text)
    if not MSUF__Chat_IsSafeString(text) then  return nil end
    local lower = string.lower(text)
    lower = string.gsub(lower, "^%s+", "")
     return lower
end
local function MSUF_ChatCommand_OnChatMsg(_, text, author, ...)
    local msgLower = MSUF__Chat_GetLowerTrimmed(text)
    if not msgLower then  return end
    -- Fast reject: only care about "!msuf help" (allow extra whitespace)
    if string.sub(msgLower, 1, 5) ~= "!msuf" then  return end
    local rest = string.sub(msgLower, 6)
    rest = string.gsub(rest, "^%s+", "")
    rest = string.gsub(rest, "%s+$", "")
    if rest ~= "help" then  return end
    if not MSUF__Chat_IsFromSelf(author, ...) then  return end
    MSUF_PrintHelp()
 end
if type(MSUF_EventBus_Register) == "function" then
    local evs = {
        "CHAT_MSG_SAY","CHAT_MSG_YELL","CHAT_MSG_PARTY","CHAT_MSG_PARTY_LEADER",
        "CHAT_MSG_RAID","CHAT_MSG_RAID_LEADER","CHAT_MSG_RAID_WARNING",
        "CHAT_MSG_INSTANCE_CHAT","CHAT_MSG_INSTANCE_CHAT_LEADER",
        "CHAT_MSG_GUILD","CHAT_MSG_OFFICER","CHAT_MSG_WHISPER",
    }
    for _, e in ipairs(evs) do
        MSUF_EventBus_Register(e, "MSUF_CHATCMD", MSUF_ChatCommand_OnChatMsg)
    end
end
SLASH_MIDNIGHTSUF1 = "/msufold"
SlashCmdList["MIDNIGHTSUF"] = function(msg)
    msg = msg and msg:lower() or ""
    msg = msg:gsub("^%s+", "")
    local cmd = msg:match("^(%S+)") or ""
    if cmd == "" or cmd == "help" then
        MSUF_PrintHelp()
         return
    end
-- Should clean this up since we have now a button for full reset.
    if cmd == "fullreset" then
        if not MSUF_FullResetPending then
            MSUF_FullResetPending = true
            print(Tr("|cffff0000MSUF WARNING:|r This will delete |cffff0000ALL|r MSUF profiles & settings for this account."))
            print(Tr("|cffffcc00MSUF:|r Type |cffffff00/msuf fullreset confirm|r to stage the reset."))
            print(Tr("|cffffcc00MSUF:|r Then click: MSUF Menu > Advanced > Factory Reset (or type /reload)."))
             return
        end
        if msg ~= "fullreset confirm" then
            MSUF_FullResetPending = false
            print(Tr("|cffffcc00MSUF:|r Full reset cancelled. If you still want it, type:"))
            print("  /msuf fullreset")
            print("  /msuf fullreset confirm")
            print(Tr("  (then /reload OR MSUF Menu > Advanced > Factory Reset)"))
             return
        end
        MSUF_FullResetPending = false
        MSUF_DoFullReset({ skipReload = true })
         return
    end
    if cmd == "reset" then
        if InCombatLockdown and InCombatLockdown() then
            print(Tr("|cffff0000MSUF:|r Cannot reset while in combat."))
             return
        end
        MSUF_Chat_RunEnsureDB()
        if MSUF_DB then
            for unit, defaults in pairs(MSUF_RESET_DEFAULTS) do
                MSUF_DB[unit] = MSUF_DB[unit] or {}
                local t = MSUF_DB[unit]
                for k, v in pairs(defaults) do
                    t[k] = v
                end
                if t.enabled == nil then
                    t.enabled = true
                end
            end
            MSUF_ResetPositionAnchorsToScreen()
        end
        if type(_G.MSUF_ApplyAllSettings_Immediate) == "function" then
            _G.MSUF_ApplyAllSettings_Immediate()
        else
            MSUF_Chat_RunApplyAllSettings()
        end
        if type(_G.MSUF_ForceReanchorAllUnitFrames_Once) == "function" then
            _G.MSUF_ForceReanchorAllUnitFrames_Once()
        end
        local updateFonts = _G.MSUF_UpdateAllFonts
        if type(updateFonts) == "function" then
            updateFonts()
        end
        print(Tr("|cff00ff00MSUF:|r Positions and visibility reset to defaults."))
         return
    end
    if cmd == "absorb" then
        MSUF_Chat_RunEnsureDB()
        local g = (type(MSUF_DB) == "table" and type(MSUF_DB.general) == "table") and MSUF_DB.general or nil
        if not g then
            print(Tr("|cffff0000MSUF:|r DB not initialized."))
             return
        end
        g.showTotalAbsorbAmount = not g.showTotalAbsorbAmount
        MSUF_Chat_RunApplyAllSettings()
        if g.showTotalAbsorbAmount then
            print(Tr("|cff00ff00MSUF:|r Total absorb amount in HP text ENABLED."))
        else
            print(Tr("|cff00ff00MSUF:|r Total absorb amount in HP text DISABLED."))
        end
         return
    end
    if cmd == "analytics" then
        if type(_G.MSUF_Analytics_HandleSlash) == "function" then
            _G.MSUF_Analytics_HandleSlash(msg:match("^%S+%s*(.-)%s*$") or "")
        else
            print(Tr("|cffff0000MSUF:|r Analytics module not loaded."))
        end
         return
    end
    if cmd == "gfhoverdebug" then
        local arg = msg:match("^%S+%s*(.-)%s*$") or ""
        arg = arg:gsub("^%s+", ""):gsub("%s+$", "")
        if arg == "" or arg == "toggle" then
            Debug.gfHover = not (Debug.gfHover == true)
        elseif arg == "on" then
            Debug.gfHover = true
        elseif arg == "off" then
            Debug.gfHover = false
        elseif arg == "status" then
            -- no-op, only print below
        else
            print(Tr("|cffff0000MSUF:|r Usage: /msuf gfhoverdebug on|off|status"))
            return
        end
        print(string.format("|cff7aa2f7MSUF|r: Group-frame hover debug is %s.", Debug.gfHover == true and "|cff73dacaON|r" or "|cfff7768eOFF|r"))
        return
    end
    if cmd == "inputdebug" then
        MSUF_PrintInputDebug()
        return
    end
    -- Unknown
    MSUF_PrintHelp()
 end
SLASH_MSUFRELOADUI1 = "/rl"
SlashCmdList["MSUFRELOADUI"] = function()
    if type(ReloadUI) == "function" then
        ReloadUI()
    end
end
local MSUF_PlayerInfoFrame
local function MSUF_GetPlayerInfoFrame()
    if MSUF_PlayerInfoFrame then
         return MSUF_PlayerInfoFrame
    end
    local f = CreateFrame("Frame", "MSUF_PlayerInfoFrame", UIParent, "BackdropTemplate")
    f:SetSize(260, 90)
    f:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOMRIGHT", -16, 180)
    f:SetFrameStrata("TOOLTIP")
    f:SetClampedToScreen(true)
    f:EnableMouse(false)
    if f.SetBackdrop then
        f:SetBackdrop({
            bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
            edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
            tile = true, tileSize = 16, edgeSize = 16,
            insets = { left = 4, right = 4, top = 4, bottom = 4 },
        })
        f:SetBackdropColor(0, 0, 0, 0.9)
    end
    local nameFS = f:CreateFontString(nil, "OVERLAY", "GameTooltipHeaderText")
    nameFS:SetPoint("TOPLEFT", 8, -8)
    nameFS:SetJustifyH("LEFT")
    nameFS:SetText("")
    nameFS:SetTextColor(1, 1, 1) -- Wei wie normaler Tooltip-Text
    local line2FS = f:CreateFontString(nil, "OVERLAY", "GameTooltipTextSmall")
    line2FS:SetPoint("TOPLEFT", nameFS, "BOTTOMLEFT", 0, -2)
    line2FS:SetJustifyH("LEFT")
    local line3FS = f:CreateFontString(nil, "OVERLAY", "GameTooltipTextSmall")
    line3FS:SetPoint("TOPLEFT", line2FS, "BOTTOMLEFT", 0, -2)
    line3FS:SetJustifyH("LEFT")
    local line4FS = f:CreateFontString(nil, "OVERLAY", "GameTooltipTextSmall")
    line4FS:SetPoint("TOPLEFT", line3FS, "BOTTOMLEFT", 0, -2)
    line4FS:SetJustifyH("LEFT")
    local line5FS = f:CreateFontString(nil, "OVERLAY", "GameTooltipTextSmall")
    line5FS:SetPoint("TOPLEFT", line4FS, "BOTTOMLEFT", 0, -2)
    line5FS:SetJustifyH("LEFT")
    line5FS:SetTextColor(0.8, 0.8, 0.8) -- leicht ausgegraut wie Location-Zeile
    f.name  = nameFS
    f.line2 = line2FS
    f.line3 = line3FS
    f.line4 = line4FS
    f.line5 = line5FS
    f:Hide()
    MSUF_PlayerInfoFrame = f
     return f
end
ns.Tooltips = ns.Tooltips or {}
local Tooltips = ns.Tooltips

local TOOLTIP_PROVIDER_GAME = "GAME"
local TOOLTIP_PROVIDER_MSUF = "MSUF"
local TOOLTIP_ANCHOR_EXTERNAL = "EXTERNAL"
local TOOLTIP_ANCHOR_FIXED = "FIXED"
local TOOLTIP_ANCHOR_CURSOR = "CURSOR"

local function MSUF_GetTooltipGeneral()
    MSUF_Chat_RunEnsureDB()
    local db = (type(MSUF_DB) == "table") and MSUF_DB or nil
    db = db or {}
    db.general = db.general or {}
    return db.general
end

local function MSUF_NormalizeTooltipSettings(g)
    if type(g) ~= "table" then
        g = MSUF_GetTooltipGeneral()
    end

    local provider = g.unitTooltipProvider
    if provider ~= TOOLTIP_PROVIDER_GAME and provider ~= TOOLTIP_PROVIDER_MSUF then
        provider = (g.disableUnitInfoTooltips == false) and TOOLTIP_PROVIDER_MSUF or TOOLTIP_PROVIDER_GAME
        g.unitTooltipProvider = provider
    end

    local anchor = g.unitTooltipAnchor
    if anchor ~= TOOLTIP_ANCHOR_EXTERNAL and anchor ~= TOOLTIP_ANCHOR_FIXED and anchor ~= TOOLTIP_ANCHOR_CURSOR then
        local legacyStyle = (g.unitInfoTooltipStyle == "modern") and TOOLTIP_ANCHOR_CURSOR or TOOLTIP_ANCHOR_FIXED
        if provider == TOOLTIP_PROVIDER_MSUF then
            anchor = legacyStyle
        elseif type(g.tooltipPosX) == "number" and type(g.tooltipPosY) == "number" then
            anchor = TOOLTIP_ANCHOR_FIXED
        elseif g.unitInfoTooltipStyle == "modern" then
            anchor = TOOLTIP_ANCHOR_CURSOR
        elseif g.disableUnitInfoTooltips == true then
            anchor = TOOLTIP_ANCHOR_EXTERNAL
        else
            anchor = TOOLTIP_ANCHOR_EXTERNAL
        end
        g.unitTooltipAnchor = anchor
    end

    if provider == TOOLTIP_PROVIDER_MSUF and anchor == TOOLTIP_ANCHOR_EXTERNAL then
        anchor = TOOLTIP_ANCHOR_FIXED
        g.unitTooltipAnchor = anchor
    end

    g.disableUnitInfoTooltips = (provider ~= TOOLTIP_PROVIDER_MSUF)
    g.unitInfoTooltipStyle = (anchor == TOOLTIP_ANCHOR_CURSOR) and "modern" or "classic"

    return provider, anchor
end

local function MSUF_ApplyTooltipFixedPoint(frame, g, defaultOffsetY)
    if not frame then return end
    frame:ClearAllPoints()
    local cx = g.tooltipPosX
    local cy = g.tooltipPosY
    if type(cx) == "number" and type(cy) == "number" then
        frame:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", cx, cy)
    else
        frame:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOMRIGHT", -16, defaultOffsetY or 16)
    end
end

local function MSUF_AnchorGameTooltip(owner, g, anchor)
    local gt = _G.GameTooltip
    if not gt or gt:IsForbidden() then return nil end

    if anchor == TOOLTIP_ANCHOR_CURSOR then
        gt:SetOwner(UIParent, "ANCHOR_CURSOR", 0, -100)
    elseif anchor == TOOLTIP_ANCHOR_FIXED then
        gt:SetOwner(UIParent, "ANCHOR_NONE")
        MSUF_ApplyTooltipFixedPoint(gt, g, 16)
    else
        if type(_G.GameTooltip_SetDefaultAnchor) == "function" then
            _G.GameTooltip_SetDefaultAnchor(gt, owner or UIParent)
        elseif owner then
            gt:SetOwner(owner, "ANCHOR_RIGHT")
        else
            gt:SetOwner(UIParent, "ANCHOR_NONE")
        end
    end

    return gt
end

local function MSUF_ClearTrackedGameTooltip(owner, force)
    local gt = _G.GameTooltip
    if not gt or gt:IsForbidden() then return end
    if (not force) and gt._msufUnitTooltipOwner and gt._msufUnitTooltipOwner ~= owner then return end
    gt._msufUnitTooltipOwner = nil
    gt._msufUnitTooltipUnit = nil
    gt:Hide()
end

if _G.GameTooltip and (not _G.GameTooltip._msufTooltipTrackingHooked) then
    _G.GameTooltip._msufTooltipTrackingHooked = true
    _G.GameTooltip:HookScript("OnHide", function(tip)
        tip._msufUnitTooltipOwner = nil
        tip._msufUnitTooltipUnit = nil
    end)
end

local function MSUF_PositionPlayerInfoFrame(frame)
    local g = MSUF_GetTooltipGeneral()
    local _, anchor = MSUF_NormalizeTooltipSettings(g)

    if anchor == TOOLTIP_ANCHOR_CURSOR and GetCursorPosition and UIParent then
        frame:ClearAllPoints()
        local x, y = GetCursorPosition()
        local scale = UIParent:GetEffectiveScale() or 1
        x, y = x / scale, y / scale
        frame:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", x - 130, y - 150)
    else
        MSUF_ApplyTooltipFixedPoint(frame, g, 180)
    end
 end
-- Tooltip helpers (unified; keeps behavior, reduces copy/paste)
local function MSUF_UnitInfo_GetLocationText()
    local zone = GetZoneText and GetZoneText() or nil
    local subzone = GetSubZoneText and GetSubZoneText() or nil
    if subzone and subzone ~= "" and zone and zone ~= "" then
        -- Only compare strings if they're safe; otherwise just prefer subzone when present.
        if (not NotSecretValue) or (NotSecretValue(subzone) and NotSecretValue(zone)) then
            if subzone ~= zone then
                 return subzone
            end
        end
    end
    return (subzone and subzone ~= "") and subzone or zone
end
local function MSUF_UnitInfo_BuildNameLine(unit, fallbackName, isPlayer)
    local nameLine = UnitName(unit) or fallbackName
    if isPlayer then
        -- Secret-safe (12.0): UnitIsAFK/UnitIsDND may return secret booleans;
        -- direct boolean test in `if` would hard-error. Guard via issecretvalue.
        local getAway = _G.MSUF_GetCachedAwayStatus
        if getAway then
            local away = getAway(unit, true, true, false)
            if away == 1 or away == 3 then
                nameLine = nameLine .. " <AFK>"
            elseif away == 2 then
                nameLine = nameLine .. " <DND>"
            end
        elseif UnitIsAFK then
            local afk = UnitIsAFK(unit)
            if not (issecretvalue and issecretvalue(afk)) and afk then
                nameLine = nameLine .. " <AFK>"
            elseif UnitIsDND then
                local dnd = UnitIsDND(unit)
                if not (issecretvalue and issecretvalue(dnd)) and dnd then
                    nameLine = nameLine .. " <DND>"
                end
            end
        end
    end
     return nameLine
end
local function MSUF_UnitInfo_BuildLine4(faction, isPVP)
    if (not faction or faction == "") and not isPVP then
         return ""
    end
    local text = faction or ""
    if isPVP then
        if text ~= "" then
            text = text .. "  PvP"
        else
            text = "PvP"
        end
    end
     return text
end
local function MSUF_UnitInfo_BuildLine2_Player(level, race, classLoc)
    local n = tonumber(level)
    if n and n > 0 then
        if race and classLoc then
            return string.format("Level %d %s %s", n, race, classLoc)
        elseif classLoc then
            return string.format("Level %d %s", n, classLoc)
        else
            return string.format("Level %d", n)
        end
    end
    return classLoc or ""
end
local function MSUF_UnitInfo_ClassificationText(classification)
    if classification == "elite" then
         return "Elite"
    elseif classification == "rare" then
         return "Rare"
    elseif classification == "rareelite" then
         return "Rare Elite"
    elseif classification == "worldboss" then
         return "Boss"
    end
     return nil
end
local function MSUF_UnitInfo_BuildLine2_NPC(level, classification)
    local n = tonumber(level)
    if not (n and n > 0) then
         return ""
    end
    local line2 = string.format("Level %d", n)
    local clsText = MSUF_UnitInfo_ClassificationText(classification)
    if clsText then
        line2 = line2 .. string.format(" (%s)", clsText)
    end
     return line2
end
local function MSUF_UnitInfo_ShowFrame(f, nameLine, line2, line3, line4, loc)
    f.name:SetText(nameLine or "")
    f.line2:SetText(line2 or "")
    f.line3:SetText(line3 or "")
    f.line4:SetText(line4 or "")
    f.line5:SetText(loc or "")
    MSUF_PositionPlayerInfoFrame(f)
    f:Show()
 end
local function MSUF_ShowUnitInfoTooltip(unit, fallbackName)
    local f = MSUF_GetPlayerInfoFrame()
    if not UnitExists(unit) then
        f:Hide()
         return
    end

    if unit == "pet" then
        local name = UnitName(unit) or fallbackName or "Pet"
        local level = UnitLevel(unit)
        local creatureType = UnitCreatureType(unit)
        local line2 = ""
        local n = tonumber(level)
        if n and n > 0 then
            line2 = string.format("Level %d", n)
        end
        MSUF_UnitInfo_ShowFrame(f, name, line2, creatureType or "", "", MSUF_UnitInfo_GetLocationText())
        return
    end

    local level = UnitLevel(unit)
    local isPlayer = UnitIsPlayer(unit)
    local race, classLoc, faction, isPVP
    if isPlayer then
        race = UnitRace(unit)
        classLoc = select(1, UnitClass(unit))
        faction = UnitFactionGroup(unit)
        isPVP = UnitIsPVP(unit)
    end

    local nameLine = MSUF_UnitInfo_BuildNameLine(unit, fallbackName, isPlayer)
    local line2 = isPlayer and MSUF_UnitInfo_BuildLine2_Player(level, race, classLoc)
        or MSUF_UnitInfo_BuildLine2_NPC(level, UnitClassification(unit))
    local line3 = isPlayer and (classLoc or "") or (UnitCreatureType(unit) or "")
    local line4 = isPlayer and MSUF_UnitInfo_BuildLine4(faction, isPVP) or ""

    if unit == "player" then
        local specName
        if GetSpecialization and GetSpecializationInfo then
            local specIndex = GetSpecialization()
            if specIndex then
                local _, sName = GetSpecializationInfo(specIndex, nil, nil, nil, UnitSex("player"))
                specName = sName
            end
        end
        if specName and classLoc then
            line3 = string.format("%s %s", specName, classLoc)
        elseif specName then
            line3 = specName
        end
    end

    MSUF_UnitInfo_ShowFrame(f, nameLine, line2, line3, line4, MSUF_UnitInfo_GetLocationText())
 end
function MSUF_ShowPlayerInfoTooltip()
    MSUF_ShowUnitInfoTooltip("player", "Player")
 end
function MSUF_ShowTargetInfoTooltip()
    MSUF_ShowUnitInfoTooltip("target", "Target")
 end
function MSUF_ShowFocusInfoTooltip()
    MSUF_ShowUnitInfoTooltip("focus", "Focus")
 end
function MSUF_ShowTargetTargetInfoTooltip()
    MSUF_ShowUnitInfoTooltip("targettarget", "Target of Target")
 end
function MSUF_ShowFocusTargetInfoTooltip()
    MSUF_ShowUnitInfoTooltip("focustarget", "Focus Target")
 end
function MSUF_ShowPetInfoTooltip()
    MSUF_ShowUnitInfoTooltip("pet", "Pet")
 end
_G.MSUF_ShowUnitInfoTooltip = MSUF_ShowUnitInfoTooltip
function MSUF_HidePlayerInfoTooltip()
    if MSUF_PlayerInfoFrame then
        -- If the Edit Mode tooltip preview is active, restore the preview
        -- instead of hiding the frame (OnLeave from unit frames must not
        -- kill the persistent drag-preview).
        if MSUF_PlayerInfoFrame._msufEditPreviewActive then
            if _G.MSUF_Tooltip_ShowEditPreview then
                _G.MSUF_Tooltip_ShowEditPreview()
            end
            return
        end
        MSUF_PlayerInfoFrame:Hide()
    end
 end
Tooltips.GetGeneral = Tooltips.GetGeneral or MSUF_GetTooltipGeneral
Tooltips.Normalize = Tooltips.Normalize or MSUF_NormalizeTooltipSettings
Tooltips.ShowUnit = Tooltips.ShowUnit or function(owner, unit, opts)
    if not unit or not UnitExists(unit) then return false end
    local g = MSUF_GetTooltipGeneral()
    local provider, anchor = MSUF_NormalizeTooltipSettings(g)
    if opts and opts.provider == TOOLTIP_PROVIDER_MSUF then
        provider = TOOLTIP_PROVIDER_MSUF
    end
    if opts and opts.anchor then
        anchor = opts.anchor
        if provider == TOOLTIP_PROVIDER_MSUF and anchor == TOOLTIP_ANCHOR_EXTERNAL then
            anchor = TOOLTIP_ANCHOR_FIXED
        end
    end

    if provider == TOOLTIP_PROVIDER_MSUF then
        MSUF_ClearTrackedGameTooltip(owner, true)
        MSUF_ShowUnitInfoTooltip(unit, opts and opts.fallbackName)
        return true
    end

    MSUF_HidePlayerInfoTooltip()
    local gt = MSUF_AnchorGameTooltip(owner, g, anchor)
    if not gt or type(gt.SetUnit) ~= "function" then return false end
    gt._msufUnitTooltipOwner = owner
    gt._msufUnitTooltipUnit = unit
    gt:SetUnit(unit)
    gt:Show()
    return true
end
Tooltips.HideUnit = Tooltips.HideUnit or function(owner)
    MSUF_HidePlayerInfoTooltip()
    MSUF_ClearTrackedGameTooltip(owner)
end
-- [8c6] Removed legacy Options UI relayout functions (Player/Bars).
-- These were dead/duplicate layout builders superseded by MSUF_Options_Core.lua.
if not _G.MSUF_SetBlizzardEditModeFromMSUF then
    function _G.MSUF_SetBlizzardEditModeFromMSUF(active)
        if InCombatLockdown and InCombatLockdown() then
             return
        end
        MSUF_Chat_RunEnsureDB()
        if MSUF_DB and MSUF_DB.general and MSUF_DB.general.linkEditModes == false then
             return
        end
        local emf = _G.EditModeManagerFrame
        if not emf then return end
        if active then
            if not _G.MSUF_BlizzEditModeStartedByMSUF then
                _G.MSUF_BlizzEditModeStartedByMSUF = true
            end
            local ok = pcall(function()
                if type(securecallfunction) == "function" and type(_G.ShowUIPanel) == "function" then
                    securecallfunction(_G.ShowUIPanel, emf) -- this will show the edit mode panel and enter edit mode
                elseif emf.Show then
                    emf:Show()
                elseif emf.EnterEditMode then
                    emf:EnterEditMode()
                end
             end)
            if not ok then
                _G.MSUF_BlizzEditModeStartedByMSUF = nil
            end
        else
            if not _G.MSUF_BlizzEditModeStartedByMSUF then return end
            _G.MSUF_BlizzEditModeStartedByMSUF = nil
            pcall(function()
                if type(securecallfunction) == "function" and type(emf.ExitEditMode) == "function" then
                    securecallfunction(emf.ExitEditMode, emf)
                elseif emf.ExitEditMode then
                    emf:ExitEditMode()
                end
                if type(securecallfunction) == "function" and type(_G.HideUIPanel) == "function" and emf.IsShown and emf:IsShown() then
                    securecallfunction(_G.HideUIPanel, emf)
                elseif emf.Hide and emf.IsShown and emf:IsShown() then
                    emf:Hide()
                end
             end)
        end
     end
end
-- [8c6] Removed PLAYER_LOGIN Options relayout hook (Bars).
ns.MSUF_UpdateAllFonts = ns.MSUF_UpdateAllFonts or _G.MSUF_UpdateAllFonts

-- ==========================================================================
-- Edit Mode: Tooltip Position Drag Handle
-- Shows a preview of the MSUF tooltip and makes it draggable to set a custom
-- position. Persists to MSUF_DB.general.tooltipPosX / .tooltipPosY.
-- Only active while MSUF Edit Mode is on; zero OnUpdate cost outside of drag.
-- ==========================================================================
do
    local tooltipDragHandle          -- overlay frame (lazy-created)
    local tooltipSettingsHandle      -- click-through-to-settings frame for non-draggable states
    local tooltipEditPreviewActive = false

    local function MSUF_Tooltip_IsSettingsOnlyPreview()
        local g = MSUF_GetTooltipGeneral()
        local provider, anchor = MSUF_NormalizeTooltipSettings(g)
        return provider == TOOLTIP_PROVIDER_GAME and anchor == TOOLTIP_ANCHOR_EXTERNAL
    end

    local function MSUF_Tooltip_OpenTooltipSettings()
        if not MSUF_Tooltip_IsSettingsOnlyPreview() then return end

        local menu = _G.MSUF2
        if menu and type(menu.Open) == "function" then
            menu.Open("opt_misc")
        elseif type(_G.MSUF2_Open) == "function" then
            _G.MSUF2_Open("opt_misc")
        elseif type(_G.MSUF_OpenStandaloneOptionsWindow) == "function" then
            _G.MSUF_OpenStandaloneOptionsWindow("opt_misc")
        elseif type(_G.MSUF_OpenPage) == "function" then
            _G.MSUF_OpenPage("opt_misc")
        end

        local function OpenSection()
            menu = _G.MSUF2
            local search = menu and menu.Search
            if search and type(search.OpenTarget) == "function" then
                search.OpenTarget("opt_misc", "Unitframe tooltips", "Unitframe tooltips", "Unitframe tooltips")
            elseif type(_G.MSUF_OpenPage) == "function" then
                _G.MSUF_OpenPage("opt_misc")
            end
        end

        if C_Timer and C_Timer.After then
            C_Timer.After(0, OpenSection)
        else
            OpenSection()
        end
    end

    -- ---- persistence -------------------------------------------------------
    local function MSUF_Tooltip_SavePosition(frame)
        if not frame then return end
        MSUF_Chat_RunEnsureDB()
        local g = MSUF_DB and MSUF_DB.general
        if not g then return end
        local left   = frame.GetLeft   and frame:GetLeft()
        local bottom = frame.GetBottom and frame:GetBottom()
        if type(left) == "number" and type(bottom) == "number" then
            g.tooltipPosX = math.floor(left + 0.5)
            g.tooltipPosY = math.floor(bottom + 0.5)
        end
    end

    local function MSUF_Tooltip_SetNudgeTarget(handle)
        if not handle or not _G.MSUF_EM2_SetPreviewNudgeTarget then return end
        _G.MSUF_EM2_SetPreviewNudgeTarget({
            frame = handle,
            IsActive = function()
                local p = handle:GetParent()
                return tooltipEditPreviewActive and handle:IsShown() and p and p.IsShown and p:IsShown()
            end,
            Nudge = function(_, dx, dy)
                local p = handle:GetParent()
                if not p then return end
                MSUF_Chat_RunEnsureDB()
                local g = MSUF_DB and MSUF_DB.general
                if not g then return end
                local left = tonumber(g.tooltipPosX)
                local bottom = tonumber(g.tooltipPosY)
                if left == nil then left = p:GetLeft() or 0 end
                if bottom == nil then bottom = p:GetBottom() or 0 end
                g.tooltipPosX = math.floor(left + (dx or 0) + 0.5)
                g.tooltipPosY = math.floor(bottom + (dy or 0) + 0.5)
                p:ClearAllPoints()
                p:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", g.tooltipPosX, g.tooltipPosY)
            end,
        })
    end

    -- ---- reset helper (called from Options / slash) ------------------------
    local function MSUF_Tooltip_ResetPosition()
        MSUF_Chat_RunEnsureDB()
        local g = MSUF_DB and MSUF_DB.general
        if not g then return end
        g.tooltipPosX = nil
        g.tooltipPosY = nil
    end
    _G.MSUF_Tooltip_ResetPosition = MSUF_Tooltip_ResetPosition

    -- ---- drag handle (lazy) ------------------------------------------------
    local function MSUF_Tooltip_EnsureDragHandle(parent)
        if tooltipDragHandle then
            -- Re-parent in case tooltip was re-created (shouldn't happen, but safe).
            if tooltipDragHandle:GetParent() ~= parent then
                tooltipDragHandle:SetParent(parent)
            end
            tooltipDragHandle:SetAllPoints(parent)
            return tooltipDragHandle
        end

        local dh = CreateFrame("Frame", "MSUF_TooltipDragHandle", parent)
        dh:SetAllPoints(parent)
        dh:EnableMouse(true)
        dh:RegisterForDrag("LeftButton")
        dh:SetFrameLevel((parent.GetFrameLevel and parent:GetFrameLevel() or 0) + 10)

        -- Subtle visual overlay so the user knows it's draggable
        local bg = dh:CreateTexture(nil, "OVERLAY")
        bg:SetAllPoints()
        bg:SetColorTexture(0.2, 0.6, 1.0, 0.12)
        dh._bg = bg

        local label = dh:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        label:SetPoint("TOP", dh, "TOP", 0, -2)
        label:SetText(Tr("Drag or arrow keys to reposition"))
        label:SetTextColor(0.4, 0.8, 1.0, 0.9)
        dh._label = label

        dh:SetScript("OnDragStart", function(self)
            if InCombatLockdown and InCombatLockdown() then return end
            MSUF_Tooltip_SetNudgeTarget(self)
            local p = self:GetParent()
            if p then
                p:SetMovable(true)
                p:SetClampedToScreen(true)
                p:StartMoving()
            end
        end)

        dh:SetScript("OnDragStop", function(self)
            local p = self:GetParent()
            if not p then return end
            p:StopMovingOrSizing()
            p:SetMovable(false)
            MSUF_Tooltip_SavePosition(p)
        end)

        dh:SetScript("OnMouseDown", function(self, button)
            if button == "LeftButton" then
                MSUF_Tooltip_SetNudgeTarget(self)
            end
        end)

        dh:Hide()
        tooltipDragHandle = dh
        return dh
    end

    local function MSUF_Tooltip_EnsureSettingsHandle(parent)
        if tooltipSettingsHandle then
            if tooltipSettingsHandle:GetParent() ~= parent then
                tooltipSettingsHandle:SetParent(parent)
            end
            tooltipSettingsHandle:SetAllPoints(parent)
            return tooltipSettingsHandle
        end

        local h = CreateFrame("Button", "MSUF_TooltipSettingsHandle", parent)
        h:SetAllPoints(parent)
        h:EnableMouse(true)
        h:RegisterForClicks("LeftButtonUp")
        h:SetFrameLevel((parent.GetFrameLevel and parent:GetFrameLevel() or 0) + 10)

        local hover = h:CreateTexture(nil, "HIGHLIGHT")
        hover:SetAllPoints()
        hover:SetColorTexture(0.2, 0.6, 1.0, 0.08)
        h._hover = hover

        h:SetScript("OnClick", MSUF_Tooltip_OpenTooltipSettings)
        tooltipSettingsHandle = h
        return h
    end

    -- ---- Edit Mode enter/exit ----------------------------------------------
    local function MSUF_Tooltip_ShowEditPreview()
        local f = MSUF_GetPlayerInfoFrame()
        if not f then return end

        local g = MSUF_GetTooltipGeneral()
        local provider, anchor = MSUF_NormalizeTooltipSettings(g)
        local blizzardControlled = (provider == TOOLTIP_PROVIDER_GAME and anchor == TOOLTIP_ANCHOR_EXTERNAL)

        if blizzardControlled then
            if tooltipDragHandle then
                tooltipDragHandle:Hide()
            end
            tooltipEditPreviewActive = false
            f:SetMovable(false)

            if f.name  then f.name:SetText(Tr("GameTooltip (addon-compatible)")) end
            if f.line2 then f.line2:SetText(Tr("Addon / Blizzard controlled")) end
            if f.line3 then f.line3:SetText(Tr("Player Name")) end
            if f.line4 then f.line4:SetText(Tr("Level 80 Human Paladin")) end
            if f.line5 then f.line5:SetText("") end

            f:ClearAllPoints()
            f:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOMRIGHT", -16, 16)
            f:Show()
            f._msufEditPreviewActive = true
            MSUF_Tooltip_EnsureSettingsHandle(f):Show()
            return
        end

        if tooltipSettingsHandle then
            tooltipSettingsHandle:Hide()
        end

        -- Fill with placeholder content so the user sees size/layout
        if f.name  then f.name:SetText(Tr("Player Name")) end
        if f.line2 then f.line2:SetText(Tr("Level 80 Human Paladin")) end
        if f.line3 then f.line3:SetText(Tr("Protection Paladin")) end
        if f.line4 then f.line4:SetText(Tr("Alliance")) end
        if f.line5 then f.line5:SetText(Tr("Stormwind City")) end

        -- Position (uses saved pos if available, else style default)
        MSUF_PositionPlayerInfoFrame(f)
        f:Show()

        -- Mark frame as in edit-preview so MSUF_HidePlayerInfoTooltip
        -- restores the preview instead of hiding it.
        f._msufEditPreviewActive = true

        -- Enable drag
        local dh = MSUF_Tooltip_EnsureDragHandle(f)
        dh:Show()
        tooltipEditPreviewActive = true
    end

    local function MSUF_Tooltip_HideEditPreview()
        if tooltipDragHandle then
            tooltipDragHandle:Hide()
        end
        if tooltipSettingsHandle then
            tooltipSettingsHandle:Hide()
        end
        tooltipEditPreviewActive = false
        -- Hide the tooltip preview (but not if a real tooltip is being shown outside edit mode).
        -- The preview uses placeholder text, so we can safely hide it.
        if MSUF_PlayerInfoFrame then
            MSUF_PlayerInfoFrame._msufEditPreviewActive = false
            MSUF_PlayerInfoFrame:SetMovable(false)
            MSUF_PlayerInfoFrame:Hide()
        end
    end

    -- Expose for CloseAllPositionPopups and external callers
    _G.MSUF_Tooltip_HideEditPreview = MSUF_Tooltip_HideEditPreview
    _G.MSUF_Tooltip_ShowEditPreview = MSUF_Tooltip_ShowEditPreview

    -- Register as AnyEditMode listener (fires on MSUF and/or Blizzard Edit Mode transitions).
    if _G.MSUF_RegisterAnyEditModeListener then
        _G.MSUF_RegisterAnyEditModeListener(function(active)
            if active then
                MSUF_Tooltip_ShowEditPreview()
            else
                MSUF_Tooltip_HideEditPreview()
            end
        end)
    end
end
