local _, private = ...

-- Lua Globals --
-- luacheck: globals floor next type tonumber max

-- Libs --
local LSM = _G.LibStub("LibSharedMedia-3.0")
local fonts = {}
for fontType, fontName in next, private.fontNames do
    fonts[fontType] = {
        name = fontName,
        path = LSM:Fetch("font", fontName)
    }
end

-- RealUI --
local RealUI = _G.RealUI
local Aurora = private.Aurora
private.isDev = RealUI.isDev

local debug = RealUI.GetDebug("Skins")
private.debug = debug

local scaleMessagePrinted = false

local auroraConfigDefaults = {
    acknowledgedSplashScreen = false,
    bags = true,
    banks = true,
    chat = true,
    loot = true,
    mainmenubar = false,
    fonts = true,
    tooltips = true,
    chatBubbles = true,
    chatBubbleNames = true,
    characterSheet = true,
    objectiveTracker = true,
    talentArtBackground = true,
    buttonsHaveGradient = true,
    customHighlight = {
        enabled = false,
        r = 0.243,
        g = 0.570,
        b = 1,
    },
    alpha = 0.5,
    hasAnalytics = true,
    customClassColors = {},
    gcMode = "smooth",

    -- DEV: run Blizzard's original GameTooltip_InsertFrame instead of Aurora's
    -- replacement. Aurora owning that global taints it for every secure reader,
    -- which blocks C_ItemUpgrade.UpgradeItem(). Toggle with /auroraInsertFrame.
    devRestoreInsertFrame = false,
}

local auroraCharacterConfigDefaults = {
    heroTalentsCustomAnchor = false,
    heroTalentsAnchorPreset = "default",
}

local auroraCharacterScopedKeys = {
    heroTalentsCustomAnchor = true,
    heroTalentsAnchorPreset = true,
}

local function DeepCopy(value)
    if type(value) ~= "table" then
        return value
    end

    local copy = {}
    for key, nestedValue in next, value do
        copy[key] = DeepCopy(nestedValue)
    end
    return copy
end

local function ApplyTableDefaults(target, defaultsTable)
    for key, defaultValue in next, defaultsTable do
        if target[key] == nil then
            target[key] = DeepCopy(defaultValue)
        elseif type(defaultValue) == "table" and type(target[key]) == "table" then
            ApplyTableDefaults(target[key], defaultValue)
        end
    end
end

local function ReplaceTableContents(target, source)
    for key in next, target do
        target[key] = nil
    end
    for key, value in next, source do
        target[key] = DeepCopy(value)
    end
end

local function EnsureProfileAuroraConfig(profile)
    local profileConfig = rawget(profile, "auroraConfig")
    if type(profileConfig) ~= "table" then
        if type(_G.AuroraConfig) == "table" then
            profileConfig = DeepCopy(_G.AuroraConfig)
            ApplyTableDefaults(profileConfig, auroraConfigDefaults)
        else
            profileConfig = DeepCopy(auroraConfigDefaults)
        end
        profile.auroraConfig = profileConfig
    else
        ApplyTableDefaults(profileConfig, auroraConfigDefaults)
    end
    return profileConfig
end

local function IsCharacterScopedAuroraKey(key)
    return auroraCharacterScopedKeys[key] == true
end

local function EnsureCharacterAuroraConfig(charConfig, profileConfig)
    local characterConfig = rawget(charConfig, "auroraConfig")
    if type(characterConfig) ~= "table" then
        characterConfig = {}
        charConfig.auroraConfig = characterConfig
    end

    for key, defaultValue in next, auroraCharacterConfigDefaults do
        if characterConfig[key] == nil then
            if profileConfig and profileConfig[key] ~= nil then
                characterConfig[key] = DeepCopy(profileConfig[key])
            else
                characterConfig[key] = DeepCopy(defaultValue)
            end
        end
    end

    return characterConfig
end

local function SyncRuntimeAuroraConfig(profile)
    local profileConfig = EnsureProfileAuroraConfig(profile)
    local characterConfig = private.skinsCharDB and EnsureCharacterAuroraConfig(private.skinsCharDB, profileConfig) or nil

    if type(_G.AuroraConfig) ~= "table" then
        _G.AuroraConfig = {}
    end

    ReplaceTableContents(_G.AuroraConfig, profileConfig)
    if characterConfig then
        for key in next, auroraCharacterScopedKeys do
            if characterConfig[key] ~= nil then
                _G.AuroraConfig[key] = DeepCopy(characterConfig[key])
            end
        end
    end
    return profileConfig, _G.AuroraConfig
end

local function GetAuroraConfigStores()
    if not private.skinsDB then
        if type(_G.AuroraConfig) ~= "table" then
            _G.AuroraConfig = {}
        end
        return nil, _G.AuroraConfig, nil
    end

    local profileConfig = EnsureProfileAuroraConfig(private.skinsDB)
    local characterConfig = private.skinsCharDB and EnsureCharacterAuroraConfig(private.skinsCharDB, profileConfig) or nil
    if type(_G.AuroraConfig) ~= "table" then
        _G.AuroraConfig = {}
    end
    return profileConfig, _G.AuroraConfig, characterConfig
end

local defaults = {
    profile = {
        stripeAlpha = 0.5,
        buttonColor = {
        },
        frameColor = {
            a = 0.7
        },
        classColors = {
        },
        uiModScale = 1,
        customScale = 1,
        isHighRes = false,
        isPixelScale = true,
        fonts = fonts,
        auroraConfig = auroraConfigDefaults,
        addons = {
            ["**"] = true
        }
    },
    char = {
        auroraConfig = auroraCharacterConfigDefaults,
    },
}

local function ProfileExists(aceDB, profileName)
    if not aceDB or not profileName then
        return false
    end

    local profiles = aceDB:GetProfiles()
    if not profiles then
        return false
    end

    for _, existingName in ipairs(profiles) do
        if existingName == profileName then
            return true
        end
    end

    return false
end

local function HasValidScaleSettings(profileData)
    if type(profileData) ~= "table" then
        return false
    end

    local uiModScale = tonumber(profileData.uiModScale)
    local customScale = tonumber(profileData.customScale)

    return uiModScale ~= nil and uiModScale > 0
        and customScale ~= nil and customScale > 0
        and type(profileData.isHighRes) == "boolean"
        and type(profileData.isPixelScale) == "boolean"
end

local function CopyScaleSettings(sourceProfile, targetProfile)
    if type(sourceProfile) ~= "table" or type(targetProfile) ~= "table" then
        return
    end

    targetProfile.uiModScale = sourceProfile.uiModScale
    targetProfile.customScale = sourceProfile.customScale
    targetProfile.isHighRes = sourceProfile.isHighRes
    targetProfile.isPixelScale = sourceProfile.isPixelScale
end

local function GetStoredProfiles(aceDB)
    if not aceDB then
        return nil
    end

    if type(aceDB.profiles) == "table" then
        return aceDB.profiles
    end

    if type(aceDB.sv) == "table" and type(aceDB.sv.profiles) == "table" then
        return aceDB.sv.profiles
    end

    return nil
end

local function EnsureCanonicalSkinsProfiles(skinsDB)
    if not skinsDB then
        return
    end

    local currentSkinsProfile = skinsDB:GetCurrentProfile()
    if currentSkinsProfile == "Default" then
        local hadRealUIProfile = ProfileExists(skinsDB, "RealUI")

        skinsDB:SetProfile("RealUI")

        if not hadRealUIProfile then
            skinsDB:CopyProfile("Default", true)
        end
    end

    local currentProfile = skinsDB:GetCurrentProfile()
    local hadHealingProfile = ProfileExists(skinsDB, "RealUI-Healing")

    if not hadHealingProfile then
        skinsDB:SetProfile("RealUI-Healing")
        if ProfileExists(skinsDB, "RealUI") then
            skinsDB:CopyProfile("RealUI", true)
        end
    end

    local profiles = GetStoredProfiles(skinsDB) or {}
    local realUIProfile = profiles["RealUI"]
    local healingProfile = profiles["RealUI-Healing"]
    local links = RealUI.db and RealUI.db.char and RealUI.db.char.scopeLinks or nil
    local skinsScopeLinked = links and links.skins == true

    if HasValidScaleSettings(realUIProfile) and (not HasValidScaleSettings(healingProfile) or not skinsScopeLinked) then
        skinsDB:SetProfile("RealUI-Healing")
        CopyScaleSettings(realUIProfile, skinsDB.profile)
    end

    skinsDB:SetProfile(currentProfile == "Default" and "RealUI" or currentProfile)
end

function RealUI.GetAuroraConfigValue(key, fallbackValue)
    local profileConfig, runtimeConfig, characterConfig = GetAuroraConfigStores()
    if IsCharacterScopedAuroraKey(key) and characterConfig then
        if characterConfig[key] == nil then
            if auroraCharacterConfigDefaults[key] ~= nil then
                characterConfig[key] = DeepCopy(auroraCharacterConfigDefaults[key])
            else
                characterConfig[key] = fallbackValue
            end
        end
        runtimeConfig[key] = DeepCopy(characterConfig[key])
        return characterConfig[key]
    end

    if profileConfig then
        if profileConfig[key] == nil then
            if auroraConfigDefaults[key] ~= nil then
                profileConfig[key] = DeepCopy(auroraConfigDefaults[key])
            else
                profileConfig[key] = fallbackValue
            end
        end
        runtimeConfig[key] = DeepCopy(profileConfig[key])
        return profileConfig[key]
    end

    if runtimeConfig[key] == nil then
        if auroraConfigDefaults[key] ~= nil then
            runtimeConfig[key] = DeepCopy(auroraConfigDefaults[key])
        else
            runtimeConfig[key] = fallbackValue
        end
    end
    return runtimeConfig[key]
end

function RealUI.SetAuroraConfigValue(key, value)
    local profileConfig, runtimeConfig, characterConfig = GetAuroraConfigStores()
    if IsCharacterScopedAuroraKey(key) and characterConfig then
        characterConfig[key] = DeepCopy(value)
        runtimeConfig[key] = DeepCopy(value)
        return
    end

    if profileConfig then
        profileConfig[key] = DeepCopy(value)
    end
    runtimeConfig[key] = DeepCopy(value)
end

function RealUI.GetAuroraConfigTable(key)
    local profileConfig, runtimeConfig = GetAuroraConfigStores()
    local defaultValue = auroraConfigDefaults[key] or {}

    if profileConfig then
        if type(profileConfig[key]) ~= "table" then
            profileConfig[key] = DeepCopy(defaultValue)
        end
        runtimeConfig[key] = DeepCopy(profileConfig[key])
        return profileConfig[key], runtimeConfig[key]
    end

    if type(runtimeConfig[key]) ~= "table" then
        runtimeConfig[key] = DeepCopy(defaultValue)
    end
    return nil, runtimeConfig[key]
end

function RealUI.SetAuroraConfigTable(key, value)
    local copiedValue = DeepCopy(value)
    local profileConfig, runtimeConfig = GetAuroraConfigStores()
    if profileConfig then
        profileConfig[key] = DeepCopy(copiedValue)
    end
    runtimeConfig[key] = copiedValue
end

RealUI.textures = private.textures

local moddedFrames, pixelScale = {}, 768 / (select(2, _G.GetPhysicalScreenSize()))
local function ResetScale(frame)
    if not private.skinsDB then return end
    if _G.InCombatLockdown() then
        -- Never touch frame scale during combat; any SetScale call from addon
        -- code can propagate taint to the action bar secure execution path.
        return
    end
    if frame.IsProtected and frame:IsProtected() then
        return
    end
    -- Frames that are sized via ModValue become HUGE with retina scale.
    if private.skinsDB.isHighRes then
        frame:SetScale(private.skinsDB.customScale)
    elseif private.skinsDB.customScale > pixelScale then
        frame:SetScale(pixelScale)
    end
end
local function UpdateModScale()
    if not private.skinsDB then return end
    private.uiScale = private.skinsDB.uiModScale
    for frame, func in next, moddedFrames do
        ResetScale(frame)
        if func then
            func(frame)
        end
    end
end

function RealUI.RegisterModdedFrame(frame, updateFunc)
    ResetScale(frame)
    moddedFrames[frame] = updateFunc or false
end

--[[ UI Scale Strategy
    The engine scale (SetCVar / UIParent:SetScale) is applied ONCE at login
    inside the deferred C_Timer.After(0) call from Aurora's ADDON_LOADED.
    This is the safest window — no combat, no protected frames active yet.

    Runtime changes from the config panel (toggling Pixel Perfect, changing
    Custom Scale, re-optimizing) save the new values and prompt a /reload.
    This avoids all taint issues while giving the optimizer full control.

    WoW CVar "uiScale" has a minimum of 0.64. For scales below that
    (e.g. pixel-perfect on 4K = 0.3556), we use UIParent:SetScale() as
    a fallback since it has no floor. This only happens at login.
]]

local uiMod = (select(2, _G.GetPhysicalScreenSize()) / 768)
local uiScaleChanging
local isStartupScaleApplied = false

function RealUI.UpdateUIScale(newScale, fromConfig)
    if uiScaleChanging then return end

    -- Ensure Aurora's standalone scale message is always suppressed when
    -- RealUI_Skins is the host addon, even if we early-return below.
    private.scaleReported = true
    _G.AURORA_SCALE_REPORTED = true

    -- Force-disable WoW's built-in "Use UI Scale" CVar so the Blizzard
    -- slider never overrides RealUI's scale management.
    if _G.GetCVar("useUiScale") == "1" then
        _G.SetCVar("useUiScale", 0)
        -- Defer notification until RealUI is fully loaded (PLAYER_LOGIN)
        _G.C_Timer.After(2, function()
            if RealUI.NotificationWithDuration then
                RealUI:NotificationWithDuration(15,
                    "UI Scale Conflict",
                    true,
                    "WoW's 'Use UI Scale' was enabled and has been disabled. "
                        .. "RealUI manages UI scaling — adjust scale in /realui > Skins instead.",
                    nil,
                    [[Interface\AddOns\RealUI\Media\Notification_Alert]]
                )
            end
        end)
    end

    -- Guard against the user re-enabling it via the Settings panel.
    -- B55/B21/B51: this was a global hooksecurefunc("SetCVar", …) — a post-
    -- hook on one of the hottest functions in the client, which marked EVERY
    -- Blizzard execution that set any CVar as tainted by RealUI_Skins from
    -- that point on. First clean taint.log (2026-08-21, delve) caught it
    -- red-handed: ShouldShowMawBuffs' aura read refused mid-tracker-update,
    -- "tainted by RealUI_Skins", on a chain this addon never touches.
    -- CVAR_UPDATE delivers the same information in OUR OWN execution instead,
    -- leaving Blizzard's untouched.
    if not private._uiScaleGuardInstalled then
        private._uiScaleGuardInstalled = true
        local guard = _G.CreateFrame("Frame")
        guard:RegisterEvent("CVAR_UPDATE")
        guard:SetScript("OnEvent", function(_, _, cvar, value)
            if cvar and cvar:lower() == "useuiscale" and tostring(value) == "1" then
                _G.C_Timer.After(0, function()
                    _G.SetCVar("useUiScale", 0)
                    RealUI.UpdateUIScale()
                    if RealUI.NotificationWithDuration then
                        RealUI:NotificationWithDuration(15,
                            "UI Scale Conflict",
                            true,
                            "WoW's 'Use UI Scale' was re-disabled. "
                                .. "RealUI manages UI scaling — adjust scale in /realui > Skins instead.",
                            nil,
                            [[Interface\AddOns\RealUI\Media\Notification_Alert]]
                        )
                    end
                end)
            end
        end)
    end

    local _, pysHeight = _G.GetPhysicalScreenSize()
    uiMod = (pysHeight / 768) * (private.uiScale or 1)
    pixelScale = 768 / pysHeight
    private.debug("pixel scale", pixelScale, uiMod)

    local oldScale = private.skinsDB.customScale
    local cvarScale = tonumber(RealUI.Scale.Round(_G.GetCVar("uiscale"), 2))
    local parentScale = RealUI.Scale.Round(_G.UIParent:GetScale(), 2)
    private.debug("current scale", oldScale, cvarScale, parentScale)

    -- Determine the desired customScale
    if private.skinsDB.isPixelScale then
        newScale = RealUI.Scale.Round(pixelScale, 2)
    end
    if not newScale then
        newScale = oldScale
    end
    private.debug("newScale", newScale)

    -- Compute the engine-level scale (what UIParent should be)
    local engineScale = newScale
    if private.skinsDB.isHighRes then
        engineScale = RealUI.Scale.Round(engineScale * 2, 2)
    end

    uiScaleChanging = true
    private.debug("update engineScale", engineScale)

    -- Apply engine scale ONLY at login (first call)
    if not isStartupScaleApplied then
        isStartupScaleApplied = true

        if parentScale ~= engineScale then
            -- At login (deferred C_Timer.After(0) context), apply the scale
            -- directly via UIParent:SetScale(). SetCVar("uiScale") from addon
            -- code may not take effect reliably and has a 0.64 floor.
            private.debug("Setting UIParent:SetScale to", engineScale)
            _G.UIParent:SetScale(engineScale)
        end
    end

    private.skinsDB.customScale = newScale
    UpdateModScale()
    uiScaleChanging = false

    -- Report effective scale on first run (login)
    if not scaleMessagePrinted then
        scaleMessagePrinted = true
        -- Also ensure Aurora's standalone message is suppressed
        private.scaleReported = true
        local phyWidth, phyHeight = _G.GetPhysicalScreenSize()
        local mode = private.skinsDB.isHighRes and "HiDPI" or (private.skinsDB.isPixelScale and "Pixel" or "Custom")
        _G.print(("Running on %sx%s - Effective Scale: %.2f (%s)"):format(phyWidth, phyHeight, newScale, mode))
    end

    -- Only prompt reload when explicitly triggered from config panel / optimizer
    if fromConfig and parentScale ~= engineScale then
        RealUI:ReloadUIDialog()
    end
end

function RealUI.GetInterfaceSize()
    local width, height = _G.GetPhysicalScreenSize()
    if private.skinsDB and private.skinsDB.isHighRes then
        return width, height, width / 2, height / 2
    end
    return width, height
end

local ScaleAPI = {}

local skinnedFrames = {}
function RealUI:IsFrameSkinned(frame)
    return not not skinnedFrames[frame]
end
function RealUI:RegisterSkinnedFrame(frame, color)
    skinnedFrames[frame] = color
end
-- Stripe alpha with a fallback, matching `defaults.profile.stripeAlpha`.
--
-- `private.skinsDB` is only assigned at the END of `private.OnLoad` (and
-- re-assigned by the profile callbacks), so it can legitimately be nil while
-- skinning runs — which is how a hover over an Infobar block threw
-- "attempt to index field 'skinsDB'" from `AddFrameStripes` in beta 9: the
-- MenuFrame tooltip pool acquires a frame, that gets skinned, and the stripes
-- hook fires with no DB behind it. Several functions in this file already
-- guard for exactly this (`if not private.skinsDB then return end`); these two
-- did not, and they are on the skinning hot path where returning early would
-- silently drop the stripe instead.
local DEFAULT_STRIPE_ALPHA = 0.5
local function GetStripeAlpha()
    local skinsDB = private.skinsDB
    return (skinsDB and skinsDB.stripeAlpha) or DEFAULT_STRIPE_ALPHA
end

function RealUI:UpdateFrameStyle()
    local skinsDB = private.skinsDB
    local frameAlpha = skinsDB and skinsDB.frameColor and skinsDB.frameColor.a
    for frame, color in next, skinnedFrames do
        if frame._stripes then
            Aurora.Base.SetBackdropColor(frame, color, frameAlpha)
            frame._stripes:SetAlpha(GetStripeAlpha())
        else
            Aurora.Base.SetBackdropColor(frame, color)
        end
    end
end
function RealUI:AddFrameStripes(Frame)
    local bg = Frame:GetBackdropTexture("bg")
    if not bg then return end
    local stripes = bg:GetParent():CreateTexture(nil, "BACKGROUND", nil, -6)
    stripes:SetTexture([[Interface\AddOns\RealUI\Media\StripesThin]], true, true)
    stripes:SetAlpha(GetStripeAlpha())
    stripes:SetAllPoints(bg)
    stripes:SetHorizTile(true)
    stripes:SetVertTile(true)
    stripes:SetBlendMode("ADD")
    Frame._stripes = stripes
end

function private.OnLoad()
    -- Bug 9 fix: Suppress Aurora's standalone "UI Scale" message immediately.
    -- This must be the FIRST thing in OnLoad, before any code that could error,
    -- so the deferred C_Timer.After(0) callback in init.lua always sees it.
    private.scaleReported = true
    -- Also set a global flag for dev mode where Aurora may load as a separate
    -- addon with its own private table that can't see our private.scaleReported.
    _G.AURORA_SCALE_REPORTED = true

    local skinsDB = _G.LibStub("AceDB-3.0"):New("RealUI_SkinsDB", defaults, "RealUI")

    EnsureCanonicalSkinsProfiles(skinsDB)

    skinsDB:RegisterCallback("OnProfileChanged", function(db)
        private.skinsDB = db.profile
        private.skinsCharDB = db.char
        if private.skinsDB then
            SyncRuntimeAuroraConfig(private.skinsDB)
        end
        RealUI:ReloadUIDialog()
    end)
    skinsDB:RegisterCallback("OnProfileCopied", function(db)
        private.skinsDB = db.profile
        private.skinsCharDB = db.char
        if private.skinsDB then
            SyncRuntimeAuroraConfig(private.skinsDB)
        end
        RealUI:ReloadUIDialog()
    end)
    skinsDB:RegisterCallback("OnProfileReset", function(db)
        private.skinsDB = db.profile
        private.skinsCharDB = db.char
        if private.skinsDB then
            SyncRuntimeAuroraConfig(private.skinsDB)
        end
        RealUI:ReloadUIDialog()
    end)
    private.skinsDB = skinsDB.profile
    private.skinsCharDB = skinsDB.char
    SyncRuntimeAuroraConfig(private.skinsDB)

    -- DEV A/B for the GameTooltip_InsertFrame taint.  Standalone Aurora exposes
    -- this as `/aurora insertframe`, but RealUI_Skins embeds only Aurora's
    -- skin XML — gui.lua and config.lua never load — so the toggle is mirrored
    -- here against the RealUI_Skins profile that feeds _G.AuroraConfig.
    -- OnLoad can run against the early RealUI stub (before the AceAddon with
    -- AceConsole exists), so registration is deferred when needed.
    local function ToggleInsertFrame()
        local profileConfig = EnsureProfileAuroraConfig(private.skinsDB)
        profileConfig.devRestoreInsertFrame = not profileConfig.devRestoreInsertFrame
        SyncRuntimeAuroraConfig(private.skinsDB)

        if profileConfig.devRestoreInsertFrame then
            _G.print("|cff00a0ffAurora:|r GameTooltip_InsertFrame — using |cff00ff00Blizzard's original|r (global untainted).")
            _G.print("  Exercise: LootHistory \"all passed\", Professions reagent/reward, delve widget")
            _G.print("  sets, Garrison mission threats, quest-offer map pins, trinket item upgrade.")
        else
            _G.print("|cff00a0ffAurora:|r GameTooltip_InsertFrame — using |cffffcc00Aurora's replacement|r (global tainted).")
        end
        RealUI:ReloadUIDialog()
    end

    if RealUI.RegisterChatCommand then
        RealUI:RegisterChatCommand("auroraInsertFrame", ToggleInsertFrame)
    else
        _G.C_Timer.After(0, function()
            local fullRealUI = _G.RealUI
            if fullRealUI and fullRealUI.RegisterChatCommand then
                fullRealUI:RegisterChatCommand("auroraInsertFrame", ToggleInsertFrame)
            end
        end)
    end

    -- Set flags
    private.disabled.bags = true
    private.disabled.banks = true
    private.disabled.mainmenubar = true
    private.disabled.pixelScale = not private.skinsDB.isPixelScale

    private.uiScale = private.skinsDB.uiModScale
    private.UpdateUIScale = RealUI.UpdateUIScale

    for fontType, font in next, private.skinsDB.fonts do
        private.font[fontType] = font.path or LSM:Fetch("font", font.name)
    end

    local Skin = Aurora.Skin
    local Color, Util = Aurora.Color, Aurora.Util

    -- Initialize custom colors
    local frameColor = private.skinsDB.frameColor
    if not frameColor.r then
        frameColor.r, frameColor.g, frameColor.b = Color.frame:GetRGB()
    else
        Color.frame:SetRGBA(frameColor.r, frameColor.g, frameColor.b, Color.frame.a)
    end
    Util.SetFrameAlpha(frameColor.a)

    local buttonColor = private.skinsDB.buttonColor
    if not buttonColor.r then
        buttonColor.r, buttonColor.g, buttonColor.b = Color.button:GetRGB()
    else
        Color.button:SetRGB(buttonColor.r, buttonColor.g, buttonColor.b)
    end

    local classColors = private.skinsDB.classColors
    if not classColors[private.charClass.token] then
        private.classColorsReset(classColors, _G.RAID_CLASS_COLORS)
    end
    private.setColorCache(classColors)

    -- Set overrides and hooks
    local C = Aurora[2]
    C.media.arrowDown = [[Interface\AddOns\RealUI_Skins\Aurora\media\arrow-down-active]]
    C.media.arrowRight = [[Interface\AddOns\RealUI_Skins\Aurora\media\arrow-right-active]]
    C.media.checked = [[Interface\AddOns\RealUI_Skins\Aurora\media\CheckButtonHilight]]
    C.media.roleIcons = [[Interface\AddOns\RealUI_Skins\Aurora\media\UI-LFG-ICON-ROLES]]

    _G.hooksecurefunc(Skin, "AzeriteEmpoweredItemUITemplate", function(Frame)
        Frame.BorderFrame.NineSlice._stripes:SetParent(Frame)
    end)

    -- Skip tooltip-family frames: Aurora's own GameTooltip skin (see
    -- Blizzard_GameTooltip.lua) deliberately avoids marking _G.GameTooltip's
    -- NineSlice as addon-touched to keep AreaPOI/widget-set processing
    -- taint-free, but EmbeddedItemTooltip and ShoppingTooltip1/2 still go
    -- through the full skin path and land here. Creating a new texture on
    -- their NineSlice via AddFrameStripes marks that frame as addon-touched,
    -- causing the same "secret number value" GetScaledRect/GetUnscaledFrameRect
    -- failures elsewhere in the tooltip/widget layout chain.
    local stripeExemptFrames = {
        [_G.GameTooltip] = true,
        [_G.EmbeddedItemTooltip] = true,
        [_G.ShoppingTooltip1] = true,
        [_G.ShoppingTooltip2] = true,
        -- B55-family (2026-08-21, first clean taint.log): a stripes texture is
        -- a RealUI_Skins-owned object planted inside the skinned frame, and
        -- secure layout code that touches it silently taints its whole
        -- execution — no global-read line in taint.log, just the fallout.
        -- Two documented victims: the objective tracker (popup block skinned
        -- via FrameTypeFrame → next dirty-update runs tainted → MawBuffs'
        -- GetAuraDataByIndex refused mid-delve) and chat config (PanelTab
        -- stripes → PEW handler writes CURRENT_CHAT_FRAME_ID tainted, which
        -- then infects every FCF_GetCurrentChatFrame reader for the session).
        -- Exempt both containers; blocks and tabs are pooled, so the check
        -- walks the parent chain.
    }
    if _G.ObjectiveTrackerFrame then
        stripeExemptFrames[_G.ObjectiveTrackerFrame] = true
    end
    if _G.ChatConfigFrame then
        stripeExemptFrames[_G.ChatConfigFrame] = true
    end

    -- Exempt by NAME as well, for frames that do not exist yet when this list
    -- is built. Everything above is load-on-login; a demand-loaded addon's
    -- frames are not there to reference, so `_G.X` would silently be nil and
    -- the exemption would never take.
    --
    -- B57 (2026-08-23): CommunitiesFrame. Aurora skins it with
    -- `ButtonFrameTemplateMinimizable` → `PortraitFrameBaseTemplate` →
    -- `NineSlicePanelTemplate` → a NineSlice layout that calls
    -- `Skin.FrameTypeFrame`, which lands here and plants a RealUI_Skins-owned
    -- stripes texture inside `CommunitiesFrame.NineSlice`. Same vector as the
    -- objective tracker and chat config above, and the only live RealUI_Skins
    -- object anywhere near the community list — which is what the
    -- `ADDON_ACTION_BLOCKED ... RealUI_Skins ... SetAvatarTexture()` blame
    -- string has been pointing at all along.
    --
    -- The object-identity entries above have the same weakness whenever their
    -- frame is not up yet, and the 2026-08-23 taint.log shows at least one of
    -- them silently failing: `CURRENT_CHAT_FRAME_ID` is STILL being tainted by
    -- RealUI_Skins at login, through
    -- `ChatConfig_UpdateChatSettings → UpdateTabDisplay → UpdateSelection`
    -- (ChatConfigFrame.lua:2425 writes it) — the exact chain the chat-config
    -- exemption above was added to stop. Listing all three by name makes the
    -- exemption independent of who loaded first.
    local stripeExemptNames = {
        CommunitiesFrame = true,
        ChatConfigFrame = true,
        ObjectiveTrackerFrame = true,
    }

    --[[ The walk has to survive FORBIDDEN frames.

         A table lookup is safe on any object, but `GetName()` and
         `GetParent()` both throw "Attempt to access forbidden object from code
         tainted by an AddOn" when the object is forbidden — and this hook runs
         from Blizzard's own tooltip path, which reaches widget frames that can
         be exactly that. The name check added for the demand-loaded frames
         introduced the first `GetName()` call on this path and with it that
         error, seen on a UIWidget tooltip hide.

         A forbidden frame is not ours to skin under any circumstances, so
         treat it as exempt and stop walking. ]]
    --[[ Never stripe anything in the tooltip family, by TYPE.

         The named entries above (GameTooltip, EmbeddedItemTooltip,
         ShoppingTooltip1/2) show the list was always trying to say "not
         tooltips" and could only name the ones somebody had been bitten by.
         It missed the rest, and the misses are expensive: `AddFrameStripes`
         does `CreateTexture` on its target, and the objective-tracker doctrine
         is explicit that an object planted inside a Blizzard update cycle
         taints it. Tooltips are displayed *from* those cycles.

         Two errors on 2026-08-24 came through this door — a forbidden widget
         tooltip (B96) and `textHeight = <secret number>` in
         `GameTooltip_AddWidgetSet` on an AreaPOI tooltip, which is precisely
         what the taint-safe GameTooltip skin was written to prevent. Both
         frames were anonymous (`SharedTooltipTemplates.xml:19`), so no name
         list could ever have caught them.

         `GetObjectType()` catches every tooltip — named, anonymous,
         demand-loaded or added in a future patch. Checked after the forbidden
         guard, since it throws on forbidden objects too. ]]
    local function IsStripeExempt(frame)
        while frame do
            if frame.IsForbidden and frame:IsForbidden() then return true end
            if stripeExemptFrames[frame] then return true end
            if frame.GetObjectType and frame:GetObjectType() == "GameTooltip" then
                return true
            end

            local name = frame.GetName and frame:GetName()
            if name and stripeExemptNames[name] then
                -- Cache the object so later checks skip the name lookup.
                stripeExemptFrames[frame] = true
                return true
            end
            frame = frame.GetParent and frame:GetParent()
        end
        return false
    end
    _G.hooksecurefunc(Skin, "FrameTypeFrame", function(Frame)
        if not Frame then return end
        if Frame.IsForbidden and Frame:IsForbidden() then return end
        if Frame._stripes then return end
        if IsStripeExempt(Frame) then return end
        RealUI:AddFrameStripes(Frame)
    end)

    _G.hooksecurefunc(Skin, "PanelTabButtonTemplate", function(Button)
        if not Button.isTopTab then
            Button:SetButtonColor(Color.frame, frameColor.a, false)
            if not IsStripeExempt(Button) then
                RealUI:AddFrameStripes(Button)
            end
        end
    end)

    _G.hooksecurefunc(private.AddOns, "Blizzard_BarbershopUI", function()
        local BarberShopFrame = _G.BarberShopFrame
        local CharCustomizeFrame = _G.CharCustomizeFrame

        BarberShopFrame.BodyTypes:SetPoint("TOP", CharCustomizeFrame, 0, -27)
        BarberShopFrame.CancelButton:SetPoint("BOTTOMLEFT", CharCustomizeFrame, 30, 15)
        -- BarberShopFrame.ResetButton - Anchored to CancelButton
        BarberShopFrame.AcceptButton:SetPoint("BOTTOMRIGHT", CharCustomizeFrame, -30, 15)
        -- BarberShopFrame.PriceFrame - Anchored to AcceptButton

        CharCustomizeFrame.Categories:ClearAllPoints()
        CharCustomizeFrame.Categories:SetPoint("RIGHT", -21, 0)
        CharCustomizeFrame.Categories:SetPoint("BOTTOM", CharCustomizeFrame.Options, "TOP", 0, 40)

        CharCustomizeFrame.Options:ClearAllPoints()
        CharCustomizeFrame.Options:SetPoint("RIGHT")
    end)

    -- Disable user selected addon skins
    for name, enabled in next, private.skinsDB.addons do
        if not name:find("RealUI") and not enabled then
            private.AddOns[name] = private.nop
        end
    end


    function private.AddOns.RealUI()
        local Skins = RealUI:NewModule("Skins")
        Skins.db = skinsDB

        if not _G.C_AddOns.IsAddOnLoaded("Ace3") then
            private.AddOns.Ace3()
        end

        -- NavBar skinning for the WorldMap is now handled entirely by the
        -- taint-safe Skin.WorldMapNavBarTemplate in Aurora's Blizzard_WorldMap
        -- skin.  Skin.NavBarTemplate uses FrameTypeButton which writes Lua
        -- functions directly onto button tables and calls HookScript, creating
        -- addon-owned state that propagates taint into the secure pin path
        -- (AcquirePin → SetPassThroughButtons).  Do NOT call it here.

        --f.f = "f" -- error for testing RealUI_Bugs
    end

    function private.AddOns.RealUI_Bugs()
        local errorFrame = _G.RealUI_ErrorFrame
        Skin.UIPanelDialogTemplate(errorFrame)
        Skin.ScrollFrameTemplate(errorFrame.ScrollFrame)
        Skin.UIPanelButtonTemplate(errorFrame.Reload)
        Skin.NavButtonPrevious(errorFrame.PreviousError)
        Skin.NavButtonNext(errorFrame.NextError)
    end
end

--[[ Copy Scale API from Aurora until the entire UI is upgraded. ]]--
function ScaleAPI.GetUIScale()
    return uiMod or 1
end

function ScaleAPI.Round(value, places)
    local mult = 10 ^ (places or 0)
    return floor(value * mult + 0.5) / mult
end

function ScaleAPI.Value(value, getFloat)
    local mult = getFloat and 100 or 1
    return floor((value * uiMod) * mult + 0.5) / mult
end

function ScaleAPI.Size(self, width, height)
    if _G.InCombatLockdown() and self.IsProtected and self:IsProtected() then
        return
    end
    if not (width and height) then
        width, height = self:GetSize()
    end
    return self:SetSize(ScaleAPI.Value(width), ScaleAPI.Value(height))
end

function ScaleAPI.Height(self, height)
    if not (height) then
        height = self:GetHeight()
    end
    return self:SetHeight(ScaleAPI.Value(height))
end

function ScaleAPI.Width(self, width)
    if not (width) then
        width = self:GetWidth()
    end
    return self:SetWidth(ScaleAPI.Value(width))
end

function ScaleAPI.Thickness(self, thickness)
    if not (thickness) then
        thickness = self:GetThickness()
    end
    return self:SetThickness(ScaleAPI.Value(thickness))
end

local ScaleArgs do
    local function pack(t, ...)
        for i = 1, _G.select("#", ...) do
            t[i] = _G.select(i, ...)
        end
    end

    local args = {}
    function ScaleArgs(self, method, ...)
        if self.debug then
            private.debug("raw args", method, ...)
        end
        _G.wipe(args)
        --[[ This function gets called A LOT, so we recycle this table
            to reduce needless garbage creation.]]
        if ... then
            pack(args, ...)
        else
            pack(args, self["Get"..method](self))
        end

        for i = 1, #args do
            if _G.type(args[i]) == "number" then
                args[i] = ScaleAPI.Value(args[i])
            end
        end
        if self.debug then
            private.debug("final args", method, _G.unpack(args))
        end
        return args
    end
end

function ScaleAPI.Point(self, ...)
    self:SetPoint(_G.unpack(ScaleArgs(self, "Point", ...)))
end

function ScaleAPI.EndPoint(self, ...)
    self:SetEndPoint(_G.unpack(ScaleArgs(self, "EndPoint", ...)))
end
function ScaleAPI.StartPoint(self, ...)
    self:SetStartPoint(_G.unpack(ScaleArgs(self, "StartPoint", ...)))
end

-- Raw methods --
local positionMethods = {
    "Size",
    "Height",
    "Width",
    "Point",
}
local mt = _G.getmetatable(_G.UIParent).__index
for _, method in next, positionMethods do
    ScaleAPI["RawSet"..method] = mt["Set"..method]
end
RealUI.Scale = ScaleAPI
