local addonName, NS = ...

-- ============================================================================
-- VAULTSKINS: SHARED SKIN CORE
-- ============================================================================

local Skin = {}
NS.Skin = Skin

-- ======================= SHARED CONSTANTS AND CACHES ========================
local WHITE_TEXTURE = "Interface\\Buttons\\WHITE8X8"
local ORNATE_BORDER = "Interface\\Tooltips\\UI-Tooltip-Border"

local SOFT_GLOW_TEXTURE = "Interface\\Buttons\\UI-Listbox-Highlight2"
local EMPTY_TABLE = {}
local WHITE_COLOR = { r = 1, g = 1, b = 1, a = 1 }
local SIMPLE_BACKDROP_CACHE = {}
local ORNATE_BACKDROP_CACHE = {}
local EDGE_BACKDROP_CACHE = {}
local HEADER_NATIVE_TEXTURE_KEYS = { "HeaderDivider", "TopDecor", "Keyhole", "Crest", "Lock", "Border", "Background" }
local MEDIA_ROOT = "Interface\\AddOns\\" .. addonName .. "\\Media\\"
local ARTWORK_FOLDER = {
    midnight = "Midnight",
    horde = "Horde",
    alliance = "Alliance",
    arcane = "Arcane",
    fel = "Fel",
    bronze = "Bronze",
    sunwell = "Sunwell",
    void = "Void",
    emerald = "EmeraldDream",
    titan = "Titan",
    frostmourne = "Frostmourne",
    venthyr = "Venthyr",
    nerubian = "Nerubian",
    elune = "Elune",
    minimal = "Minimal",
    class = "Class",
}


local errorState = {}
local ERROR_DISABLE_THRESHOLD = 4
local REVEAL_ERROR_RECOVERY_TIMER = "reveal-error-recovery"
local REVEAL_SAFETY_TIMER = "reveal-safety-timeout"
local REVEAL_WATCHDOG_TIMER = "reveal-completion-watchdog"
local MEMORY_CLEANUP_TIMER = "memory-cleanup"
local REVEAL_SAFETY_DELAYS = {
    click = 120,
    jackpot = 30,
    default = 20,
}
local rawTimerAfter = C_Timer and C_Timer.After

-- ======================== TIMER AND ERROR PROTECTION ========================
local function ScheduleOwnedTimer(key, delay, callback)
    if NS.Stability and NS.Stability.Schedule then
        return NS.Stability:Schedule(key, delay, callback)
    end
    if rawTimerAfter then
        return rawTimerAfter(math.max(0, tonumber(delay) or 0), callback)
    end
    return callback()
end

local function CancelOwnedTimer(key)
    if NS.Stability and NS.Stability.Cancel then
        return NS.Stability:Cancel(key)
    end
    return false
end

local function CancelOwnedTimers(reason)
    if NS.Stability and NS.Stability.CancelAll then
        return NS.Stability:CancelAll(reason)
    end
    return 0
end

local function RecordStabilityMetric(metric, amount)
    if NS.Stability and NS.Stability.Record then
        NS.Stability:Record(metric, amount or 1)
    end
end

local function IsOwnedTimerPending(key)
    return NS.Stability and NS.Stability.IsPending and NS.Stability:IsPending(key) or false
end

-- Use incremental collection to avoid a full-GC hitch after reveal effects.
local function RequestIncrementalMemoryCleanup(delay, stepSize)
    if type(collectgarbage) ~= "function" then return end
    ScheduleOwnedTimer(MEMORY_CLEANUP_TIMER, delay or 0, function()
        collectgarbage("step", math.max(128, tonumber(stepSize) or 768))
    end)
end

local function ScheduleRevealRecovery(showMessage)
    ScheduleOwnedTimer(REVEAL_ERROR_RECOVERY_TIMER, 0, function()
        local skin = NS.Skin
        if not skin or not skin.StopOpeningAnimation then return end
        skin:StopOpeningAnimation()
        if showMessage then
            print("|cffffaa55VaultSkins:|r Reveal recovered; all Vault rewards were restored.")
        end
    end)
end

local function SafeCall(label, callback, ...)
    local state = errorState[label]
    if state and state.disabled then
        return false
    end

    local ok, err = pcall(callback, ...)
    if ok then
        return true
    end

    state = state or { count = 0 }
    state.count = state.count + 1
    state.lastError = tostring(err)
    state.lastTime = GetTime and GetTime() or 0
    state.disabled = state.count >= ERROR_DISABLE_THRESHOLD
    errorState[label] = state

    if NS.Skin and NS.Skin.ActivateCosmeticFallback then
        pcall(NS.Skin.ActivateCosmeticFallback, NS.Skin, label, state.lastError)
    end

    if state.count == 1 then
        print("|cffff5555VaultSkins:|r Protected a failing update (" .. tostring(label) .. ").")
        print("|cffaaaaaa" .. state.lastError .. "|r")
    elseif state.disabled then
        print("|cffff5555VaultSkins:|r Disabled repeated failures from " .. tostring(label) .. ". Use /vskin repair after updating addons.")
        if (label == "coalesced theme apply" or label == "initial skin" or label == "manual repair") and C_Timer and C_Timer.After then
            ScheduleOwnedTimer(nil, 0, function()
                if NS.Skin then
                    NS.Skin.sessionFallback = true
                    NS.Skin:RestoreOriginalUI(true)
                    print("|cffffaa55VaultSkins:|r Switched to the original Great Vault for this session.")
                end
            end)
        end
    end

    if label == "advanced card reveal" then
        ScheduleRevealRecovery(false)
    elseif label == "opening animation" or label == "initial opening animation" then
        ScheduleRevealRecovery(true)
    end
    return false
end

-- =============================== DIAGNOSTICS ================================
function Skin:GetErrorDiagnostics()
    local entries, total, disabled = {}, 0, 0
    for label, state in pairs(errorState) do
        total = total + (state.count or 0)
        if state.disabled then disabled = disabled + 1 end
        entries[#entries + 1] = {
            label = label,
            count = state.count or 0,
            disabled = state.disabled and true or false,
            lastError = state.lastError,
        }
    end
    table.sort(entries, function(first, second) return first.label < second.label end)
    return { entries = entries, total = total, disabled = disabled }
end

function Skin:ResetErrorDiagnostics()
    for key in pairs(errorState) do errorState[key] = nil end
end

function Skin:PrintErrorDiagnostics()
    local diagnostics = self:GetErrorDiagnostics()
    if diagnostics.total == 0 then
        print("|cff9f78ffVaultSkins:|r No protected errors recorded this session.")
        return
    end
    print(string.format("|cff9f78ffVaultSkins:|r %d protected error(s), %d disabled update(s).", diagnostics.total, diagnostics.disabled))
    for _, entry in ipairs(diagnostics.entries) do
        print(string.format("  %s: %d%s", entry.label, entry.count, entry.disabled and " (disabled)" or ""))
        if entry.lastError then print("    |cff888888" .. tostring(entry.lastError) .. "|r") end
    end
end

function Skin:ActivateCosmeticFallback(label, err)
    self.sessionDisabledFeatures = self.sessionDisabledFeatures or {}
    local lower = string.lower(tostring(label or ""))
    local feature
    if lower:find("background") or lower:find("scene") then
        feature = "backgroundScene"
    elseif lower:find("frame effect") or lower:find("frame idle") or lower:find("frame preview") then
        feature = "frameEffects"
    elseif lower:find("rarity") or lower:find("special reward") or lower:find("jackpot") or lower:find("flourish") then
        feature = "rewardRarity"
    end
    if not feature or self.sessionDisabledFeatures[feature] then return false end

    self.sessionDisabledFeatures[feature] = tostring(err or "protected cosmetic failure")
    if feature == "backgroundScene" then
        pcall(self.StopBackgroundScene, self, true, true)
    elseif feature == "frameEffects" then
        pcall(self.StopFrameIdleAnimation, self)
    end
    print("|cffffaa55VaultSkins:|r Disabled " .. feature .. " effects for this session; the Vault remains usable.")
    return true
end

function Skin:HandleScheduledError(key, err)
    SafeCall("scheduled " .. tostring(key), function()
        error(tostring(err), 0)
    end)
end

function Skin:GetCardSkinDiagnostics()
    local result = { complete = 0, failed = 0, partial = 0, total = 0 }
    local frame = WeeklyRewardsFrame
    if not frame or not frame.Activities then return result end
    for _, activity in ipairs(frame.Activities) do
        if activity then
            result.total = result.total + 1
            if activity.VaultSkins and activity.VaultSkins.__complete then
                result.complete = result.complete + 1
            elseif activity.VaultSkinsBuildFailed then
                result.failed = result.failed + 1
            elseif activity.VaultSkinsBuilding then
                result.partial = result.partial + 1
            end
        end
    end
    return result
end

function Skin:PrintDebugDiagnostics(reset)
    if reset then
        self:ResetErrorDiagnostics()
        if NS.Stability and NS.Stability.ResetMetrics then NS.Stability:ResetMetrics() end
        print("|cff9f78ffVaultSkins:|r Debug counters reset.")
        return
    end

    local stability = NS.Stability and NS.Stability:GetDiagnostics() or {}
    local cards = self:GetCardSkinDiagnostics()
    local errors = self:GetErrorDiagnostics()
    local frame = WeeklyRewardsFrame
    local summary = self.rewardRaritySummary or (NS.RewardRarity and NS.RewardRarity:GetSummary()) or {}
    local disabled = self.sessionDisabledFeatures or {}
    local disabledText = "none"
    local disabledNames = {}
    for name in pairs(disabled) do disabledNames[#disabledNames + 1] = name end
    table.sort(disabledNames)
    if #disabledNames > 0 then disabledText = table.concat(disabledNames, ", ") end

    print("|cff9f78ffVaultSkins Debug|r")
    local themeID = NS.GetActiveThemeID and NS:GetActiveThemeID() or "unknown"
    local compatibility = NS.Compatibility and NS.Compatibility:GetStatusText() or "unknown"
    print(string.format("  Version %s · theme:%s · frame loaded:%s shown:%s · enabled:%s fallback:%s", tostring(NS.VERSION or "?"), tostring(themeID), frame and "yes" or "no", frame and frame:IsShown() and "yes" or "no", self:IsEnabled() and "yes" or "no", self.sessionFallback and "yes" or "no"))
    print(string.format("  Compatibility: %s · last apply: %s · last timer cancel: %s", tostring(compatibility), tostring(self.lastApplyReason or "none"), tostring(stability.lastCancelReason or "none")))
    print(string.format("  Scheduler pending:%d heap:%d peak:%d pool:%d · scheduled:%d executed:%d cancelled:%d", stability.pending or 0, stability.heapSize or 0, stability.peakPending or 0, stability.poolSize or 0, stability.scheduled or 0, stability.executed or 0, stability.cancelled or 0))
    print(string.format("  Scheduler replaced:%d suppressed:%d compactions:%d · reveal GC:%s", stability.replaced or 0, stability.suppressed or 0, stability.heapCompactions or 0, stability.incrementalGC and "active" or "off"))
    print(string.format("  Refreshes theme:%d requests / %d applied · rarity:%d · duplicates skipped:%d", stability.themeApplyRequests or 0, stability.themeAppliesExecuted or 0, stability.rarityRefreshRequests or 0, stability.skippedDuplicateRefreshes or 0))
    print(string.format("  Cards %d/%d complete · %d failed · %d partial", cards.complete, cards.total, cards.failed, cards.partial))
    print(string.format("  Animations created:%d reused:%d · reward choices:%d jackpot:%d", stability.animationGroupsCreated or 0, stability.animationGroupsReused or 0, summary.rewardCount or 0, summary.jackpotLevel or 0))
    print(string.format("  Protected errors:%d disabled updates:%d · session-disabled cosmetics: %s", errors.total or 0, errors.disabled or 0, disabledText))
    if stability.lastTimerError then print("  Last timer error: " .. tostring(stability.lastTimerError)) end
    print("|cffaaaaaaUse /vskin errors for labels, /vskin debug reset to clear counters, or /vskin repair to rebuild the skin.|r")
end

-- ===================== NATIVE ARTWORK AND COLOR HELPERS =====================
local function SetColor(texture, color)
    if texture and color then
        texture:SetColorTexture(color.r, color.g, color.b, color.a or 1)
    end
end

local function SetVertexColor(texture, color)
    if texture and color then
        texture:SetVertexColor(color.r, color.g, color.b, color.a or 1)
    end
end

local function HideVisual(region)
    if region then
        region:SetAlpha(0)
    end
end

local function ShowVisual(region, alpha)
    if not region then return end
    if region.Show then region:Show() end
    region:SetAlpha(alpha or 1)
end

local function CaptureAndSuppressNativeTexture(frame, region)
    if not frame or not region or not region.GetObjectType or region:GetObjectType() ~= "Texture" then return end
    frame.VaultSkinsNativeArtworkAlpha = frame.VaultSkinsNativeArtworkAlpha or setmetatable({}, { __mode = "k" })
    local cache = frame.VaultSkinsNativeArtworkAlpha
    if cache[region] == nil then
        cache[region] = region.GetAlpha and region:GetAlpha() or 1
    end
    region:SetAlpha(0)
end

local SuppressNativeTextureTree

local function SuppressNativeRegions(frame, ...)
    for index = 1, select("#", ...) do
        CaptureAndSuppressNativeTexture(frame, select(index, ...))
    end
end

local function SuppressNativeChildren(frame, ...)
    for index = 1, select("#", ...) do
        local child = select(index, ...)
        local objectType = child and child.GetObjectType and child:GetObjectType() or nil
        if child and objectType ~= "Button" and child ~= frame.CloseButton then
            SuppressNativeTextureTree(frame, child)
        end
    end
end

SuppressNativeTextureTree = function(frame, container)
    if not frame or not container then return end
    if container.GetRegions then
        SuppressNativeRegions(frame, container:GetRegions())
    end
    if container.GetChildren then
        SuppressNativeChildren(frame, container:GetChildren())
    end
end

local function SuppressNativeVaultArtwork(frame)
    if not frame then return end

    SuppressNativeTextureTree(frame, frame.BorderContainer)

    if frame.HeaderFrame then
        for _, key in ipairs(HEADER_NATIVE_TEXTURE_KEYS) do
            CaptureAndSuppressNativeTexture(frame, frame.HeaderFrame[key])
        end
    end

    CaptureAndSuppressNativeTexture(frame, frame.BorderShadow)
    CaptureAndSuppressNativeTexture(frame, frame.Divider1)
    CaptureAndSuppressNativeTexture(frame, frame.Divider2)
end

local function RestoreNativeVaultArtwork(frame)
    local cache = frame and frame.VaultSkinsNativeArtworkAlpha
    if not cache then return end
    for region, alpha in pairs(cache) do
        if region and region.SetAlpha then
            region:SetAlpha(alpha or 1)
        end
    end
end

local function RestoreVisualAlpha(region, alpha)
    if region and region.SetAlpha then region:SetAlpha(alpha or 1) end
end

local function TintRegionChildren(color, ...)
    for index = 1, select("#", ...) do
        local child = select(index, ...)
        if child and child.SetVertexColor then
            child:SetVertexColor(color.r, color.g, color.b, 1)
        end
    end
end

local function TintVisual(region, color, alpha)
    if not region then
        return
    end

    ShowVisual(region, alpha)

    if color and region.SetVertexColor then
        region:SetVertexColor(color.r, color.g, color.b, 1)
    elseif color and region.GetRegions then
        TintRegionChildren(color, region:GetRegions())
    end
end

local function ScaleColor(color, multiplier, alpha)
    return {
        r = math.min(1, math.max(0, color.r * multiplier)),
        g = math.min(1, math.max(0, color.g * multiplier)),
        b = math.min(1, math.max(0, color.b * multiplier)),
        a = alpha or color.a or 1,
    }
end

local function BlendColor(first, second, amount, alpha)
    amount = amount or 0.5
    return {
        r = first.r + (second.r - first.r) * amount,
        g = first.g + (second.g - first.g) * amount,
        b = first.b + (second.b - first.b) * amount,
        a = alpha or 1,
    }
end

local function ClampNumber(value, minimum, maximum)
    value = tonumber(value)
    if not value then return minimum end
    return math.max(minimum, math.min(maximum, value))
end

local function ColorLuma(color)
    if not color then return 1 end
    return 0.2126 * (color.r or 1) + 0.7152 * (color.g or 1) + 0.0722 * (color.b or 1)
end

local function EnsureReadableColor(color, minimumLuma)
    if not color then return WHITE_COLOR end
    minimumLuma = minimumLuma or 0.76
    local luma = ColorLuma(color)
    if luma >= minimumLuma then
        return { r = color.r, g = color.g, b = color.b, a = color.a or 1 }
    end
    local boost = ClampNumber((minimumLuma - luma) * 1.65, 0, 0.48)
    return BlendColor(color, WHITE_COLOR, boost, color.a or 1)
end

-- ========================== THEME STYLE RESOLUTION ==========================
local function GetThemeQualityProfile(theme)
    local profiles = NS.ThemeQualityProfiles
    if type(profiles) ~= "table" or not theme then return EMPTY_TABLE end
    return profiles[theme.id] or EMPTY_TABLE
end

local function GetEffectiveThemeStyle(theme, sectionName)
    if not theme then return EMPTY_TABLE end
    local cache = theme.__vaultSkinsEffectiveStyles
    if cache and cache[sectionName] then
        return cache[sectionName]
    end

    local result = {}
    local base = theme[sectionName]
    if type(base) == "table" then
        for key, value in pairs(base) do result[key] = value end
    end
    local quality = GetThemeQualityProfile(theme)
    local override = quality and quality[sectionName]
    if type(override) == "table" then
        for key, value in pairs(override) do result[key] = value end
    end

    if sectionName == "cardStyle" then
        result.normalEdgeSize = 11
        result.selectedEdgeSize = 13
        result.glowOutset = ClampNumber(result.glowOutset or 2, 0, 4)
        result.rewardGlowAlpha = ClampNumber(result.rewardGlowAlpha or 0.18, 0.10, 0.22)
        result.selectedGlowAlpha = ClampNumber(result.selectedGlowAlpha or 0.34, 0.26, 0.38)
        result.innerBorderAlpha = ClampNumber(result.innerBorderAlpha or 0.24, 0.18, 0.30)
        result.shadowBorderAlpha = ClampNumber(result.shadowBorderAlpha or 0.70, 0.56, 0.74)
        result.headerNormalAlpha = ClampNumber(result.headerNormalAlpha or 0.36, 0.32, 0.42)
        result.headerRewardAlpha = ClampNumber(result.headerRewardAlpha or 0.30, 0.26, 0.40)
        result.leftAccentWidth = ClampNumber(result.leftAccentWidth or 3, 2, 4)
    elseif sectionName == "frameStyle" then
        result.edgeSize = ClampNumber(result.edgeSize or 19, 18, 20)
        result.outerGlowEdgeSize = ClampNumber(result.outerGlowEdgeSize or 6, 4, 6)
        result.topGlowAlpha = ClampNumber(result.topGlowAlpha or 0.38, 0.32, 0.44)
        result.sideGlowAlpha = ClampNumber(result.sideGlowAlpha or 0.16, 0.12, 0.18)
        result.cornerAlpha = ClampNumber(result.cornerAlpha or 0.76, 0.68, 0.82)
        result.headerBandAlpha = ClampNumber(result.headerBandAlpha or 0.66, 0.58, 0.74)
        result.centerMistAlpha = ClampNumber(result.centerMistAlpha or 0.10, 0.06, 0.14)
    end

    cache = cache or {}
    theme.__vaultSkinsEffectiveStyles = cache
    cache[sectionName] = result
    return result
end

local function StyleValue(theme, sectionName, key, fallback)
    local section = GetEffectiveThemeStyle(theme, sectionName)
    local value = section[key]
    if value == nil then return fallback end
    return value
end

-- ======================= BACKDROP AND FRAME FACTORIES =======================
local function GetSimpleBackdrop(borderSize)
    borderSize = tonumber(borderSize) or 1
    local backdrop = SIMPLE_BACKDROP_CACHE[borderSize]
    if not backdrop then
        backdrop = { bgFile = WHITE_TEXTURE, edgeFile = WHITE_TEXTURE, edgeSize = borderSize }
        SIMPLE_BACKDROP_CACHE[borderSize] = backdrop
    end
    return backdrop
end

local function GetOrnateBackdrop(edgeSize)
    edgeSize = tonumber(edgeSize) or 14
    local backdrop = ORNATE_BACKDROP_CACHE[edgeSize]
    if not backdrop then
        backdrop = {
            bgFile = WHITE_TEXTURE,
            edgeFile = ORNATE_BORDER,
            edgeSize = edgeSize,
            insets = { left = 3, right = 3, top = 3, bottom = 3 },
        }
        ORNATE_BACKDROP_CACHE[edgeSize] = backdrop
    end
    return backdrop
end

local function GetEdgeOnlyBackdrop(edgeSize)
    edgeSize = tonumber(edgeSize) or 1
    local backdrop = EDGE_BACKDROP_CACHE[edgeSize]
    if not backdrop then
        backdrop = { edgeFile = WHITE_TEXTURE, edgeSize = edgeSize }
        EDGE_BACKDROP_CACHE[edgeSize] = backdrop
    end
    return backdrop
end

local function ApplyBackdrop(frame, background, border, borderSize)
    frame:SetBackdrop(GetSimpleBackdrop(borderSize))
    frame:SetBackdropColor(background.r, background.g, background.b, background.a or 1)
    frame:SetBackdropBorderColor(border.r, border.g, border.b, border.a or 1)
end

local function ApplyOrnateBackdrop(frame, background, border, edgeSize)
    frame:SetBackdrop(GetOrnateBackdrop(edgeSize))
    frame:SetBackdropColor(background.r, background.g, background.b, background.a or 1)
    frame:SetBackdropBorderColor(border.r, border.g, border.b, border.a or 1)
end

local function ApplyStyledBackdrop(frame, background, border, style, edgeSize)
    if style and style.ornate == false then
        ApplyBackdrop(frame, background, border, math.max(1, math.min(4, edgeSize or 1)))
    else
        ApplyOrnateBackdrop(frame, background, border, edgeSize)
    end
end

local function CreateBackdropFrame(parent, inset, levelOffset)
    if not parent or not parent.IsObjectType then return nil end
    local frameParent = parent
    if not (parent:IsObjectType("Frame") or parent:IsObjectType("Button")) then
        frameParent = parent.GetParent and parent:GetParent() or nil
    end
    if not frameParent or not frameParent.IsObjectType or not frameParent:IsObjectType("Frame") then
        return nil
    end
    local frame = CreateFrame("Frame", nil, frameParent, "BackdropTemplate")
    inset = inset or 0
    frame:SetPoint("TOPLEFT", parent, "TOPLEFT", inset, -inset)
    frame:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", -inset, inset)
    local baseLevel = 0
    if parent.GetFrameLevel then
        baseLevel = parent:GetFrameLevel()
    elseif frameParent.GetFrameLevel then
        baseLevel = frameParent:GetFrameLevel()
    end
    frame:SetFrameLevel(math.max(0, baseLevel + (levelOffset or -1)))
    frame:EnableMouse(false)
    return frame
end

local function CreateLine(parent, layer)
    local texture = parent:CreateTexture(nil, layer or "BORDER")
    texture:SetTexture(WHITE_TEXTURE)
    return texture
end

local function ResolveArtworkColor(theme, profile, key, fallbackKey)
    local value = profile and profile[key]
    if type(value) == "table" and value.r then
        return value
    end
    if type(value) == "string" and theme[value] then
        return theme[value]
    end
    return theme[fallbackKey] or theme.accent or WHITE_COLOR
end

local function ClampTextureSubLevel(subLevel)
    subLevel = tonumber(subLevel) or 0
    subLevel = math.floor(subLevel)
    return math.max(-8, math.min(7, subLevel))
end

local function CreateThemeAsset(parent, layer, subLevel)
    local texture = parent:CreateTexture(nil, layer or "BORDER", nil, ClampTextureSubLevel(subLevel))
    texture:SetAlpha(0)
    texture:Hide()
    return texture
end

-- ========================== THEME ASSET RESOLUTION ==========================
-- Keys omit file extensions because WoW resolves supported texture formats.
local SHARED_ASSET_OVERRIDES = {
    ["header"] = {
        fel = "header_a", alliance = "header_a", horde = "header_a",
        class = "header_a", midnight = "header_a",
        bronze = "header_b", arcane = "header_b",
    },
    ["corner"] = { class = "corner_a", midnight = "corner_a" },
    ["card_corner"] = { class = "card_corner_a", midnight = "card_corner_a" },
    ["lock_frame"] = { class = "lock_frame_a", midnight = "lock_frame_a" },
}

local function ThemeAssetPath(profileID, fileName)
    local overrides = SHARED_ASSET_OVERRIDES[fileName]
    local sharedFile = overrides and overrides[profileID]
    if sharedFile then
        return MEDIA_ROOT .. "Shared\\" .. sharedFile
    end
    local folder = ARTWORK_FOLDER[profileID] or ARTWORK_FOLDER.midnight
    return MEDIA_ROOT .. folder .. "\\" .. fileName
end

local function SceneAssetPath(profileID, fileName)
    profileID = string.lower(tostring(profileID or "midnight"))
    if not ARTWORK_FOLDER[profileID] then profileID = "midnight" end
    return MEDIA_ROOT .. "Scenes\\" .. profileID .. "\\" .. fileName
end

local function ConfigureThemeAsset(texture, path, point, relative, relativePoint, x, y, width, height, color, alpha, texCoord)
    if not texture then return end
    texture:ClearAllPoints()
    texture:SetTexture(path)
    texture:SetPoint(point, relative, relativePoint or point, x or 0, y or 0)
    texture:SetSize(width, height)
    texture:SetBlendMode("BLEND")
    if color then texture:SetVertexColor(color.r, color.g, color.b, 1) end
    if texCoord then texture:SetTexCoord(unpack(texCoord)) else texture:SetTexCoord(0, 1, 0, 1) end
    texture:SetAlpha(alpha or 1)
    texture:Show()
end

-- ======================== BACKGROUND SCENE PROFILES =========================
local SCENE_PROFILES = {
    midnight = { kind = "mist", primary = "glow", secondary = "accent", mistAlpha = 0.62, symbolAlpha = 0.32, rayAlpha = 0.24, driftX = 48, driftY = 8, duration = 13.5, particles = 8, particleMode = "float" },
    class = { kind = "mist", primary = "glow", secondary = "accent", mistAlpha = 0.58, symbolAlpha = 0.28, rayAlpha = 0.20, driftX = 44, driftY = 8, duration = 13.0, particles = 7, particleMode = "float" },
    horde = { kind = "embers", primary = "accent", secondary = "metal", mistAlpha = 0.56, symbolAlpha = 0.22, rayAlpha = 0.16, driftX = 12, driftY = 32, duration = 10.5, particles = 10, particleMode = "rise" },
    alliance = { kind = "rays", primary = "glow", secondary = "accent", mistAlpha = 0.54, symbolAlpha = 0.24, rayAlpha = 0.42, driftX = 22, driftY = 6, duration = 14.5, particles = 5, particleMode = "float" },
    arcane = { kind = "runes", primary = "accent", secondary = "success", mistAlpha = 0.60, symbolAlpha = 0.44, rayAlpha = 0.30, driftX = 34, driftY = 12, duration = 11.0, particles = 10, particleMode = "orbit" },
    fel = { kind = "smoke", primary = "glow", secondary = "success", mistAlpha = 0.60, symbolAlpha = 0.28, rayAlpha = 0.20, driftX = 40, driftY = 18, duration = 11.5, particles = 10, particleMode = "rise" },
    bronze = { kind = "time", primary = "glow", secondary = "accent", mistAlpha = 0.52, symbolAlpha = 0.36, rayAlpha = 0.18, driftX = 22, driftY = 8, duration = 16.0, particles = 7, particleMode = "float" },
    sunwell = { kind = "rays", primary = "glow", secondary = "accent", mistAlpha = 0.54, symbolAlpha = 0.38, rayAlpha = 0.38, driftX = 18, driftY = 5, duration = 15.0, particles = 7, particleMode = "float" },
    void = { kind = "void", primary = "glow", secondary = "accent", mistAlpha = 0.34, symbolAlpha = 0.38, rayAlpha = 0.18, driftX = 20, driftY = 5, duration = 16.0, particles = 6, particleMode = "orbit" },
    emerald = { kind = "dream", primary = "glow", secondary = "success", mistAlpha = 0.58, symbolAlpha = 0.34, rayAlpha = 0.18, driftX = 28, driftY = 12, duration = 15.5, particles = 8, particleMode = "float" },
    titan = { kind = "order", primary = "glow", secondary = "accent", mistAlpha = 0.46, symbolAlpha = 0.40, rayAlpha = 0.26, driftX = 16, driftY = 5, duration = 17.0, particles = 6, particleMode = "float" },
    frostmourne = { kind = "frost", primary = "glow", secondary = "silver", mistAlpha = 0.64, symbolAlpha = 0.40, rayAlpha = 0.24, driftX = 26, driftY = 10, duration = 14.5, particles = 10, particleMode = "float" },
    venthyr = { kind = "smoke", primary = "glow", secondary = "accent", mistAlpha = 0.62, symbolAlpha = 0.38, rayAlpha = 0.20, driftX = 18, driftY = 24, duration = 12.5, particles = 8, particleMode = "rise" },
    nerubian = { kind = "web", primary = "glow", secondary = "accent", mistAlpha = 0.52, symbolAlpha = 0.44, rayAlpha = 0.26, driftX = 12, driftY = 6, duration = 16.5, particles = 8, particleMode = "orbit" },
    elune = { kind = "rays", primary = "glow", secondary = "silver", mistAlpha = 0.58, symbolAlpha = 0.40, rayAlpha = 0.40, driftX = 20, driftY = 8, duration = 17.5, particles = 7, particleMode = "float" },
    minimal = { kind = "static", primary = "border", secondary = "accent", mistAlpha = 0.22, symbolAlpha = 0.12, rayAlpha = 0.08, driftX = 0, driftY = 0, duration = 18.0, particles = 0, particleMode = "none" },
}

local function GetBackgroundSceneProfile(theme)
    local id = theme and theme.artworkProfile and theme.artworkProfile.id or "midnight"
    id = string.lower(tostring(id or "midnight"))
    if theme and theme.__vaultSkinsSceneProfileID == id and theme.__vaultSkinsSceneProfile then
        return theme.__vaultSkinsSceneProfile, id
    end

    local base = SCENE_PROFILES[id] or SCENE_PROFILES.midnight
    local profile = {}
    for key, value in pairs(base) do profile[key] = value end
    local quality = GetThemeQualityProfile(theme)
    local override = quality and quality.backgroundSceneProfile
    if type(override) == "table" then
        for key, value in pairs(override) do profile[key] = value end
    end
    if theme then
        theme.__vaultSkinsSceneProfileID = id
        theme.__vaultSkinsSceneProfile = profile
    end
    return profile, id
end

local function GetBackgroundSceneColor(theme, key, fallback)
    local value = theme and theme[key]
    if type(value) == "table" and value.r ~= nil then return value end
    value = theme and theme[fallback or "accent"]
    if type(value) == "table" and value.r ~= nil then return value end
    return { r = 1, g = 1, b = 1, a = 1 }
end

local function StopSceneTexture(texture, hide)
    if not texture then return end
    if texture.VaultSkinsSceneDrift and texture.VaultSkinsSceneDrift:IsPlaying() then texture.VaultSkinsSceneDrift:Stop() end
    if texture.VaultSkinsScenePulse and texture.VaultSkinsScenePulse:IsPlaying() then texture.VaultSkinsScenePulse:Stop() end
    if texture.VaultSkinsSceneParticle and texture.VaultSkinsSceneParticle:IsPlaying() then texture.VaultSkinsSceneParticle:Stop() end
    if hide then texture:SetAlpha(0); texture:Hide() end
end

-- ========================= SCENE ANIMATION HELPERS ==========================
local function ConfigureSceneDrift(texture, fromAlpha, toAlpha, duration, xOffset, yOffset, delay)
    local group = texture.VaultSkinsSceneDrift
    local created = false
    if not group then
        group = texture:CreateAnimationGroup()
        created = true
        texture.VaultSkinsSceneDrift = group
        group:SetLooping("BOUNCE")
        group.Move = group:CreateAnimation("Translation")
        group.Move:SetOrder(1)
        group.Alpha = group:CreateAnimation("Alpha")
        group.Alpha:SetOrder(1)
        group:SetScript("OnPlay", function() texture:Show(); texture:SetAlpha(group.FromAlpha or 0) end)
        group:SetScript("OnStop", function() texture:SetAlpha(group.BaseAlpha or 0); if group.HideOnStop then texture:Hide() end end)
    end
    RecordStabilityMetric(created and "animationGroupsCreated" or "animationGroupsReused")
    if group:IsPlaying() then group:Stop() end
    group.FromAlpha = fromAlpha
    group.BaseAlpha = fromAlpha
    group.HideOnStop = false
    group.Move:SetOffset(xOffset or 0, yOffset or 0)
    group.Move:SetDuration(duration or 12)
    group.Move:SetStartDelay(delay or 0)
    group.Move:SetSmoothing("IN_OUT")
    group.Alpha:SetFromAlpha(fromAlpha)
    group.Alpha:SetToAlpha(toAlpha)
    group.Alpha:SetDuration(duration or 12)
    group.Alpha:SetStartDelay(delay or 0)
    group.Alpha:SetSmoothing("IN_OUT")
    return group
end

local function ConfigureScenePulse(texture, fromAlpha, toAlpha, duration, delay)
    local group = texture.VaultSkinsScenePulse
    local created = false
    if not group then
        group = texture:CreateAnimationGroup()
        created = true
        texture.VaultSkinsScenePulse = group
        group:SetLooping("BOUNCE")
        group.Alpha = group:CreateAnimation("Alpha")
        group.Alpha:SetOrder(1)
        group:SetScript("OnPlay", function() texture:Show(); texture:SetAlpha(group.FromAlpha or 0) end)
        group:SetScript("OnStop", function() texture:SetAlpha(group.BaseAlpha or 0); if group.HideOnStop then texture:Hide() end end)
    end
    RecordStabilityMetric(created and "animationGroupsCreated" or "animationGroupsReused")
    if group:IsPlaying() then group:Stop() end
    group.FromAlpha = fromAlpha
    group.BaseAlpha = fromAlpha
    group.HideOnStop = false
    group.Alpha:SetFromAlpha(fromAlpha)
    group.Alpha:SetToAlpha(toAlpha)
    group.Alpha:SetDuration(duration or 6)
    group.Alpha:SetStartDelay(delay or 0)
    group.Alpha:SetSmoothing("IN_OUT")
    return group
end

local function ConfigureSceneParticle(texture, peakAlpha, duration, xOffset, yOffset, delay)
    local group = texture.VaultSkinsSceneParticle
    local created = false
    if not group then
        group = texture:CreateAnimationGroup()
        created = true
        texture.VaultSkinsSceneParticle = group
        group:SetLooping("REPEAT")
        group.Move = group:CreateAnimation("Translation")
        group.Move:SetOrder(1)
        group.FadeIn = group:CreateAnimation("Alpha")
        group.FadeIn:SetOrder(1)
        group.Hold = group:CreateAnimation("Alpha")
        group.Hold:SetOrder(2)
        group.FadeOut = group:CreateAnimation("Alpha")
        group.FadeOut:SetOrder(3)
        group:SetScript("OnPlay", function() texture:Show(); texture:SetAlpha(0) end)
        group:SetScript("OnStop", function() texture:SetAlpha(0); texture:Hide() end)
    end
    RecordStabilityMetric(created and "animationGroupsCreated" or "animationGroupsReused")
    if group:IsPlaying() then group:Stop() end
    local fade = math.max(0.25, (duration or 5) * 0.20)
    group.Move:SetOffset(xOffset or 0, yOffset or 40)
    group.Move:SetDuration(duration or 5)
    group.Move:SetStartDelay(delay or 0)
    group.Move:SetSmoothing("NONE")
    group.FadeIn:SetFromAlpha(0)
    group.FadeIn:SetToAlpha(peakAlpha)
    group.FadeIn:SetDuration(fade)
    group.FadeIn:SetStartDelay(delay or 0)
    group.Hold:SetFromAlpha(peakAlpha)
    group.Hold:SetToAlpha(peakAlpha)
    group.Hold:SetDuration(math.max(0.2, (duration or 5) - (fade * 2)))
    group.FadeOut:SetFromAlpha(peakAlpha)
    group.FadeOut:SetToAlpha(0)
    group.FadeOut:SetDuration(fade)
    return group
end

-- ============================== SKIN STATE API ==============================
function Skin:IsEnabled()
    return VaultSkinsDB and VaultSkinsDB.skinEnabled ~= false and not self.sessionFallback
end

function Skin:GetTheme()
    local themeName = NS.GetActiveThemeID and NS:GetActiveThemeID() or (VaultSkinsDB and VaultSkinsDB.theme) or "Midnight"
    return NS:GetTheme(themeName)
end

-- ========================== MAIN SKIN CONSTRUCTION ==========================
function Skin:CreateMainSkin(frame)
    if frame.VaultSkins then
        return frame.VaultSkins
    end

    local skin = {}
    frame.VaultSkins = skin
    skin.Original = {}
    if frame.HeaderFrame and frame.HeaderFrame.Text then
        local r, g, b, a = frame.HeaderFrame.Text:GetTextColor()
        local font, size, flags = frame.HeaderFrame.Text:GetFont()
        skin.Original.HeaderColor = { r = r, g = g, b = b, a = a }
        skin.Original.HeaderFont = { font = font, size = size, flags = flags }
    end
    if frame.SelectRewardButton and frame.SelectRewardButton.Text then
        local r, g, b, a = frame.SelectRewardButton.Text:GetTextColor()
        local font, size, flags = frame.SelectRewardButton.Text:GetFont()
        skin.Original.SelectColor = { r = r, g = g, b = b, a = a }
        skin.Original.SelectFont = { font = font, size = size, flags = flags }
    end

    skin.OuterShadow = CreateFrame("Frame", nil, frame, "BackdropTemplate")
    skin.OuterShadow:SetPoint("TOPLEFT", frame, "TOPLEFT", -7, 7)
    skin.OuterShadow:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", 7, -7)
    skin.OuterShadow:SetFrameLevel(math.max(0, frame:GetFrameLevel() - 4))
    skin.OuterShadow:EnableMouse(false)

    skin.Backdrop = CreateBackdropFrame(frame, 3, -3)
    skin.Backdrop:SetFrameStrata(frame:GetFrameStrata())

    skin.MetalOuter = CreateBackdropFrame(frame, 9, -2)
    skin.Inner = CreateBackdropFrame(frame, 17, -1)

    skin.BackgroundTexture = frame:CreateTexture(nil, "BACKGROUND", nil, -7)
    skin.BackgroundTexture:SetTexture("Interface\\Tooltips\\UI-Tooltip-Background")
    skin.BackgroundTexture:SetPoint("TOPLEFT", frame, "TOPLEFT", 18, -18)
    skin.BackgroundTexture:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -18, 18)

    skin.SceneLayer = CreateFrame("Frame", nil, frame)
    skin.SceneLayer:SetPoint("TOPLEFT", frame, "TOPLEFT", 22, -104)
    skin.SceneLayer:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -22, 26)
    skin.SceneLayer:SetFrameStrata(frame:GetFrameStrata())
    skin.SceneLayer:SetFrameLevel(frame:GetFrameLevel() + 1)
    skin.SceneLayer:EnableMouse(false)
    if skin.SceneLayer.SetClipsChildren then skin.SceneLayer:SetClipsChildren(true) end

    skin.BackgroundScene = {
        MistA = skin.SceneLayer:CreateTexture(nil, "BACKGROUND", nil, 0),
        MistB = skin.SceneLayer:CreateTexture(nil, "BACKGROUND", nil, 1),
        Rays = skin.SceneLayer:CreateTexture(nil, "BACKGROUND", nil, 2),
        SymbolA = skin.SceneLayer:CreateTexture(nil, "BORDER", nil, 0),
        SymbolB = skin.SceneLayer:CreateTexture(nil, "BORDER", nil, 1),
        Particles = {},
    }
    for _, texture in ipairs({ skin.BackgroundScene.MistA, skin.BackgroundScene.MistB, skin.BackgroundScene.SymbolA, skin.BackgroundScene.SymbolB, skin.BackgroundScene.Rays }) do
        texture:SetAlpha(0)
        texture:Hide()
    end
    local sceneParticlePoints = {
        { 0.12, 0.18, 18 }, { 0.22, 0.58, 14 }, { 0.34, 0.36, 18 }, { 0.44, 0.72, 13 },
        { 0.54, 0.24, 14 }, { 0.63, 0.56, 19 }, { 0.71, 0.40, 14 }, { 0.79, 0.68, 18 },
        { 0.85, 0.22, 14 }, { 0.91, 0.50, 13 }, { 0.58, 0.82, 13 }, { 0.31, 0.80, 14 },
    }
    for index, point in ipairs(sceneParticlePoints) do
        local particle = skin.SceneLayer:CreateTexture(nil, "ARTWORK", nil, 1)
        particle:SetBlendMode("ADD")
        particle:SetSize(point[3], point[3])
        particle:SetPoint("BOTTOMLEFT", skin.SceneLayer, "BOTTOMLEFT", skin.SceneLayer:GetWidth() * point[1], skin.SceneLayer:GetHeight() * point[2])
        particle:SetAlpha(0)
        particle:Hide()
        particle.VaultSkinsSceneIndex = index
        particle.VaultSkinsScenePoint = point
        skin.BackgroundScene.Particles[index] = particle
    end

    skin.HeaderBand = CreateFrame("Frame", nil, frame, "BackdropTemplate")
    skin.HeaderBand:SetPoint("TOPLEFT", frame, "TOPLEFT", 112, -19)
    skin.HeaderBand:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -112, -19)
    skin.HeaderBand:SetHeight(98)
    skin.HeaderBand:SetFrameLevel(math.max(0, frame:GetFrameLevel() - 1))
    skin.HeaderBand:EnableMouse(false)

    skin.TopGlow = frame:CreateTexture(nil, "BACKGROUND", nil, -1)
    skin.TopGlow:SetTexture("Interface\\Buttons\\UI-Listbox-Highlight2")
    skin.TopGlow:SetBlendMode("ADD")
    skin.TopGlow:SetPoint("TOPLEFT", frame, "TOPLEFT", 28, -20)
    skin.TopGlow:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -28, -20)
    skin.TopGlow:SetHeight(118)

    skin.CenterMist = frame:CreateTexture(nil, "BACKGROUND", nil, -2)
    skin.CenterMist:SetTexture("Interface\\Buttons\\UI-Listbox-Highlight2")
    skin.CenterMist:SetBlendMode("ADD")
    skin.CenterMist:SetPoint("CENTER", frame, "CENTER", 135, 15)
    skin.CenterMist:SetSize(720, 360)

    skin.TopAccent = CreateLine(frame, "BORDER")
    skin.TopAccent:SetPoint("TOPLEFT", frame, "TOPLEFT", 42, -31)
    skin.TopAccent:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -42, -31)
    skin.TopAccent:SetHeight(2)

    skin.HeaderDivider = CreateLine(frame, "BORDER")
    skin.HeaderDivider:SetPoint("TOPLEFT", frame, "TOPLEFT", 156, -108)
    skin.HeaderDivider:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -156, -108)
    skin.HeaderDivider:SetHeight(2)

    skin.HeaderDividerGlow = frame:CreateTexture(nil, "ARTWORK", nil, 2)
    skin.HeaderDividerGlow:SetTexture("Interface\\Buttons\\UI-Listbox-Highlight2")
    skin.HeaderDividerGlow:SetBlendMode("ADD")
    skin.HeaderDividerGlow:SetPoint("CENTER", skin.HeaderDivider, "CENTER", 0, 0)
    skin.HeaderDividerGlow:SetSize(560, 28)
    skin.HeaderDividerGlow:SetAlpha(0)

    skin.BottomAccent = CreateLine(frame, "BORDER")
    skin.BottomAccent:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 42, 29)
    skin.BottomAccent:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -42, 29)
    skin.BottomAccent:SetHeight(1)

    skin.SideGlowLeft = frame:CreateTexture(nil, "BACKGROUND", nil, -1)
    skin.SideGlowLeft:SetTexture("Interface\\Buttons\\UI-Listbox-Highlight2")
    skin.SideGlowLeft:SetBlendMode("ADD")
    skin.SideGlowLeft:SetPoint("TOPLEFT", frame, "TOPLEFT", 4, -32)
    skin.SideGlowLeft:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 4, 32)
    skin.SideGlowLeft:SetWidth(34)

    skin.SideGlowRight = frame:CreateTexture(nil, "BACKGROUND", nil, -1)
    skin.SideGlowRight:SetTexture("Interface\\Buttons\\UI-Listbox-Highlight2")
    skin.SideGlowRight:SetBlendMode("ADD")
    skin.SideGlowRight:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -4, -32)
    skin.SideGlowRight:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -4, 32)
    skin.SideGlowRight:SetWidth(34)

    skin.CrestPlate = CreateFrame("Frame", nil, frame, "BackdropTemplate")
    skin.CrestPlate:SetSize(80, 58)
    skin.CrestPlate:SetPoint("TOP", frame, "TOP", 0, 3)
    skin.CrestPlate:SetFrameLevel(math.max(0, frame:GetFrameLevel() - 1))
    skin.CrestPlate:EnableMouse(false)

    skin.CrestGlow = CreateFrame("Frame", nil, frame, "BackdropTemplate")
    skin.CrestGlow:SetSize(94, 70)
    skin.CrestGlow:SetPoint("CENTER", skin.CrestPlate, "CENTER", 0, 0)
    skin.CrestGlow:SetFrameLevel(math.max(0, frame:GetFrameLevel() - 2))
    skin.CrestGlow:EnableMouse(false)
    skin.CrestGlow:SetBackdrop(GetEdgeOnlyBackdrop(5))
    skin.CrestGlow:SetBackdropColor(0, 0, 0, 0)
    skin.CrestGlow:SetAlpha(0)

    local function CreateCorner(horizontalPoint, verticalPoint, xDirection, yDirection)
        local corner = {}
        corner.Horizontal = CreateLine(frame, "BORDER")
        corner.Horizontal:SetSize(72, 2)
        corner.Horizontal:SetPoint(horizontalPoint, frame, horizontalPoint, 18 * xDirection, -18 * yDirection)

        corner.Vertical = CreateLine(frame, "BORDER")
        corner.Vertical:SetSize(2, 54)
        corner.Vertical:SetPoint(verticalPoint, frame, verticalPoint, 18 * xDirection, -18 * yDirection)
        return corner
    end

    skin.Corners = {
        CreateCorner("TOPLEFT", "TOPLEFT", 1, 1),
        CreateCorner("TOPRIGHT", "TOPRIGHT", -1, 1),
        CreateCorner("BOTTOMLEFT", "BOTTOMLEFT", 1, -1),
        CreateCorner("BOTTOMRIGHT", "BOTTOMRIGHT", -1, -1),
    }


    skin.AssetArtwork = {
        Crest = CreateThemeAsset(frame, "ARTWORK", 2),
        Header = CreateThemeAsset(frame, "BORDER", 4),
        Watermark = CreateThemeAsset(frame, "BACKGROUND", -2),
        Corners = {
            CreateThemeAsset(frame, "BORDER", 3),
            CreateThemeAsset(frame, "BORDER", 3),
            CreateThemeAsset(frame, "BORDER", 3),
            CreateThemeAsset(frame, "BORDER", 3),
        },
    }


    local function CreateFrameTrace(width, height)
        local trace = frame:CreateTexture(nil, "BORDER", nil, 6)
        trace:SetTexture("Interface\\Buttons\\UI-Listbox-Highlight2")
        trace:SetBlendMode("ADD")
        trace:SetSize(width, height)
        trace:SetAlpha(0)
        trace:Hide()
        return trace
    end

    skin.FrameTraceTop = CreateFrameTrace(170, 18)
    skin.FrameTraceLeft = CreateFrameTrace(18, 118)
    skin.FrameTraceRight = CreateFrameTrace(18, 118)
    skin.FrameTraceBottom = CreateFrameTrace(170, 14)

    skin.FrameSparkles = {}
    local sparklePoints = {
        { "TOPLEFT", 82, -41, 3 },
        { "TOPLEFT", 176, -28, 2 },
        { "TOP", -118, -46, 3 },
        { "TOP", 118, -46, 3 },
        { "TOPRIGHT", -176, -28, 2 },
        { "TOPRIGHT", -82, -41, 3 },
        { "BOTTOMLEFT", 34, 39, 2 },
        { "BOTTOMRIGHT", -34, 39, 2 },
    }
    for index, point in ipairs(sparklePoints) do
        local sparkle = frame:CreateTexture(nil, "BACKGROUND", nil, 5)
        sparkle:SetTexture(WHITE_TEXTURE)
        sparkle:SetBlendMode("ADD")
        sparkle:SetSize(point[4], point[4])
        sparkle:SetPoint(point[1], frame, point[1], point[2], point[3])
        sparkle:SetAlpha(0)
        sparkle:Hide()
        sparkle.VaultSkinsIndex = index
        skin.FrameSparkles[index] = sparkle
    end

    skin.OpeningGlow = CreateFrame("Frame", nil, frame, "BackdropTemplate")
    skin.OpeningGlow:SetPoint("TOPLEFT", frame, "TOPLEFT", 3, -3)
    skin.OpeningGlow:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -3, 3)
    skin.OpeningGlow:SetFrameLevel(frame:GetFrameLevel() + 30)
    skin.OpeningGlow:EnableMouse(false)
    skin.OpeningGlow:SetBackdrop(GetEdgeOnlyBackdrop(7))
    skin.OpeningGlow:SetBackdropColor(0, 0, 0, 0)
    skin.OpeningGlow:SetAlpha(0)
    skin.OpeningGlow:Hide()

    return skin
end

-- ======================= THEME BUTTON AND VISIBILITY ========================
function Skin:CreateThemeButton(frame)
    if frame.VaultSkinsThemeButton then
        return frame.VaultSkinsThemeButton
    end

    local button = CreateFrame("Button", nil, frame, "BackdropTemplate")
    frame.VaultSkinsThemeButton = button
    button:SetSize(74, 22)
    button:SetPoint("TOPLEFT", frame, "TOPLEFT", 38, -40)
    button:SetFrameLevel(5600)

    button.Text = button:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    button.Text:SetPoint("CENTER", 0, 0)
    button.Text:SetText((NS.L and NS.L.SKINS) or "SKINS")

    button:SetScript("OnClick", function()
        if NS.Options then
            NS.Options:Toggle()
        end
    end)
    button:SetScript("OnEnter", function(self)
        local theme = Skin:GetTheme()
        self:SetBackdropBorderColor(theme.accent.r, theme.accent.g, theme.accent.b, 1)
        local readable = EnsureReadableColor(theme.text, 0.82)
        self.Text:SetTextColor(readable.r, readable.g, readable.b, 1)
        GameTooltip:SetOwner(self, "ANCHOR_TOP")
        GameTooltip:SetText("VaultSkins")
        GameTooltip:AddLine((NS.L and NS.L.CHOOSE_THEME) or "Choose a Great Vault theme.", 1, 1, 1)
        GameTooltip:Show()
    end)
    button:SetScript("OnLeave", function(self)
        GameTooltip:Hide()
        Skin:StyleThemeButton(self)
    end)

    return button
end

function Skin:StyleThemeButton(button)
    if not button then return end
    local theme = self:GetTheme()
    ApplyBackdrop(button, BlendColor(theme.background, theme.panel, 0.62, 0.98), {
        r = theme.border.r, g = theme.border.g, b = theme.border.b, a = 0.76,
    }, 1)
    local readable = EnsureReadableColor(theme.text, 0.82)
    button.Text:SetTextColor(readable.r, readable.g, readable.b, 0.92)
end


local function SetRegionShown(region, shown)
    if not region then return end
    if shown then region:Show() else region:Hide() end
end

function Skin:SetCustomVisualsShown(shown)
    local frame = WeeklyRewardsFrame
    if not frame then return end
    local main = frame.VaultSkins
    if main then
        local keys = { "OuterShadow", "Backdrop", "MetalOuter", "Inner", "BackgroundTexture", "HeaderBand", "TopGlow", "CenterMist", "TopAccent", "HeaderDivider", "BottomAccent", "SideGlowLeft", "SideGlowRight", "CrestPlate" }
        for _, key in ipairs(keys) do SetRegionShown(main[key], shown) end
        if main.Corners then
            for _, corner in ipairs(main.Corners) do
                SetRegionShown(corner.Horizontal, shown)
                SetRegionShown(corner.Vertical, shown)
            end
        end
        if main.AssetArtwork then
            SetRegionShown(main.AssetArtwork.Crest, shown)
            SetRegionShown(main.AssetArtwork.Header, shown)
            SetRegionShown(main.AssetArtwork.Watermark, shown)
            for _, texture in ipairs(main.AssetArtwork.Corners or {}) do SetRegionShown(texture, shown) end
        end
        if main.BackgroundScene then
            if shown then
                self:ApplyBackgroundScene(self:GetTheme(), false)
            else
                self:StopBackgroundScene(true, true)
            end
        end
        if main.FrameSparkles then
            for _, sparkle in ipairs(main.FrameSparkles) do SetRegionShown(sparkle, false) end
        end
        for _, key in ipairs({ "FrameTraceTop", "FrameTraceLeft", "FrameTraceRight", "FrameTraceBottom" }) do
            SetRegionShown(main[key], false)
        end
        if not shown then
            self:StopFrameIdleAnimation()
            SetRegionShown(main.CrestGlow, false)
            SetRegionShown(main.HeaderDividerGlow, false)
            SetRegionShown(main.OpeningGlow, false)
        end
    end
    local rows = { frame.RaidFrame, frame.MythicFrame, frame.PVPFrame, frame.WorldFrame }
    for _, row in ipairs(rows) do
        if row and row.VaultSkins then
            for _, key in ipairs({ "Panel", "Shade", "Outline", "Accent", "TopLine", "BottomLine" }) do SetRegionShown(row.VaultSkins[key], shown) end
        end
    end
    if frame.Activities then
        for _, activity in ipairs(frame.Activities) do
            local skin = activity and activity.VaultSkins
            if skin then
                for _, key in ipairs({ "Shadow", "Card", "Inner", "HeaderShade", "LeftAccent", "TopLine", "BottomLine", "Glow", "Dim", "RewardShimmer", "SelectedGlow", "SelectedOutline", "SelectedBadge", "LockPlate", "LockMist" }) do SetRegionShown(skin[key], shown) end
                if skin.AssetCardCorners then for _, texture in ipairs(skin.AssetCardCorners) do SetRegionShown(texture, shown) end end
                if skin.AssetLockFrame then SetRegionShown(skin.AssetLockFrame, shown and skin.LockPlate:IsShown()) end
                SetRegionShown(skin.Hover, false)
                SetRegionShown(skin.RevealBorder, false)
                SetRegionShown(skin.RevealFlash, false)
                if not shown and skin.RewardIdleGroup and skin.RewardIdleGroup:IsPlaying() then skin.RewardIdleGroup:Stop() end
                if not shown and skin.SelectedPulseGroup and skin.SelectedPulseGroup:IsPlaying() then skin.SelectedPulseGroup:Stop() end
            end
        end
    end
    if frame.VaultSkinsThemeButton then frame.VaultSkinsThemeButton:SetShown(shown) end
    if frame.SelectRewardButton and frame.SelectRewardButton.VaultSkins then frame.SelectRewardButton.VaultSkins:SetShown(shown) end
end

-- ========================= ORIGINAL UI RESTORATION ==========================
function Skin:RestoreOriginalUI(sessionFallback)
    local frame = WeeklyRewardsFrame
    if not frame then return end
    if NS.Layout then NS.Layout:Restore(frame) end
    self:StopOpeningAnimation()
    self:SetCustomVisualsShown(false)
    self:RestoreWeeklyRewardConfirmationPopups()
    RestoreNativeVaultArtwork(frame)
    ShowVisual(frame.Background, 1)
    ShowVisual(frame.BorderShadow, 1)
    ShowVisual(frame.Divider1, 1)
    ShowVisual(frame.Divider2, 1)
    if frame.BorderContainer then
        TintVisual(frame.BorderContainer.Border, { r = 1, g = 1, b = 1 }, 1)
        TintVisual(frame.BorderContainer.TopDecor, { r = 1, g = 1, b = 1 }, 1)
    end
    if frame.HeaderFrame then TintVisual(frame.HeaderFrame.HeaderDivider, { r = 1, g = 1, b = 1 }, 1) end
    if frame.SelectRewardButton and frame.SelectRewardButton.Background then ShowVisual(frame.SelectRewardButton.Background, 1) end
    local original = frame.VaultSkins and frame.VaultSkins.Original
    if original and frame.HeaderFrame and frame.HeaderFrame.Text then
        local c, f = original.HeaderColor, original.HeaderFont
        if c then frame.HeaderFrame.Text:SetTextColor(c.r, c.g, c.b, c.a or 1) end
        if f and f.font then frame.HeaderFrame.Text:SetFont(f.font, f.size or 24, f.flags or "") end
    end
    if original and frame.SelectRewardButton and frame.SelectRewardButton.Text then
        local c, f = original.SelectColor, original.SelectFont
        if c then frame.SelectRewardButton.Text:SetTextColor(c.r, c.g, c.b, c.a or 1) end
        if f and f.font then frame.SelectRewardButton.Text:SetFont(f.font, f.size or 14, f.flags or "") end
    end
    local rows = { frame.RaidFrame, frame.MythicFrame, frame.PVPFrame, frame.WorldFrame }
    for _, row in ipairs(rows) do
        if row then
            TintVisual(row.Background, { r = 1, g = 1, b = 1 }, 1)
            TintVisual(row.Border, { r = 1, g = 1, b = 1 }, 1)
            local rowSkin = row.VaultSkins
            if row.Name and rowSkin then
                local c, f = rowSkin.OriginalNameColor, rowSkin.OriginalNameFont
                if c then row.Name:SetTextColor(c.r, c.g, c.b, c.a or 1) end
                if f and f.font then row.Name:SetFont(f.font, f.size or 18, f.flags or "") end
            end
        end
    end
    if frame.Activities then
        for _, activity in ipairs(frame.Activities) do
            if activity then
                RestoreVisualAlpha(activity.Background, 1)
                RestoreVisualAlpha(activity.Border, 1)
                RestoreVisualAlpha(activity.ItemGlow, 1)
                RestoreVisualAlpha(activity.UncollectedGlow, 1)
                RestoreVisualAlpha(activity.SelectedTexture, 1)
                RestoreVisualAlpha(activity.SelectionGlow, 1)
                if activity.UnselectedFrame then activity.UnselectedFrame:SetAlpha(1) end
                if activity.Threshold then activity.Threshold:Show() end
                activity:SetAlpha(1)
            end
        end
    end
    self:ApplyScale(true)
end

function Skin:SetSkinEnabled(enabled)
    if VaultSkinsDB then VaultSkinsDB.skinEnabled = enabled and true or false end
    self.sessionFallback = false
    if enabled then
        self:SetCustomVisualsShown(true)
        SafeCall("toggle skin", self.ApplyTheme, self, false)
        self:HookWeeklyRewardConfirmationPopups()
    else
        self:RestoreOriginalUI(false)
    end
end


-- ========================== SHARED MODULE EXPORTS ===========================
-- Export shared helpers once so focused modules do not duplicate implementations.
local Shared = NS.SkinShared or {}
NS.SkinShared = Shared
Shared.WHITE_TEXTURE = WHITE_TEXTURE
Shared.ORNATE_BORDER = ORNATE_BORDER
Shared.SOFT_GLOW_TEXTURE = SOFT_GLOW_TEXTURE
Shared.EMPTY_TABLE = EMPTY_TABLE
Shared.WHITE_COLOR = WHITE_COLOR
Shared.MEDIA_ROOT = MEDIA_ROOT
Shared.ARTWORK_FOLDER = ARTWORK_FOLDER
Shared.REVEAL_ERROR_RECOVERY_TIMER = REVEAL_ERROR_RECOVERY_TIMER
Shared.REVEAL_SAFETY_TIMER = REVEAL_SAFETY_TIMER
Shared.REVEAL_WATCHDOG_TIMER = REVEAL_WATCHDOG_TIMER
Shared.MEMORY_CLEANUP_TIMER = MEMORY_CLEANUP_TIMER
Shared.REVEAL_SAFETY_DELAYS = REVEAL_SAFETY_DELAYS
Shared.ScheduleOwnedTimer = ScheduleOwnedTimer
Shared.CancelOwnedTimer = CancelOwnedTimer
Shared.CancelOwnedTimers = CancelOwnedTimers
Shared.RecordStabilityMetric = RecordStabilityMetric
Shared.IsOwnedTimerPending = IsOwnedTimerPending
Shared.RequestIncrementalMemoryCleanup = RequestIncrementalMemoryCleanup
Shared.ScheduleRevealRecovery = ScheduleRevealRecovery
Shared.SafeCall = SafeCall
Shared.SetColor = SetColor
Shared.SetVertexColor = SetVertexColor
Shared.HideVisual = HideVisual
Shared.ShowVisual = ShowVisual
Shared.CaptureAndSuppressNativeTexture = CaptureAndSuppressNativeTexture
Shared.SuppressNativeRegions = SuppressNativeRegions
Shared.SuppressNativeChildren = SuppressNativeChildren
Shared.SuppressNativeVaultArtwork = SuppressNativeVaultArtwork
Shared.RestoreNativeVaultArtwork = RestoreNativeVaultArtwork
Shared.RestoreVisualAlpha = RestoreVisualAlpha
Shared.TintRegionChildren = TintRegionChildren
Shared.TintVisual = TintVisual
Shared.ScaleColor = ScaleColor
Shared.BlendColor = BlendColor
Shared.ClampNumber = ClampNumber
Shared.ColorLuma = ColorLuma
Shared.EnsureReadableColor = EnsureReadableColor
Shared.GetThemeQualityProfile = GetThemeQualityProfile
Shared.GetEffectiveThemeStyle = GetEffectiveThemeStyle
Shared.StyleValue = StyleValue
Shared.GetSimpleBackdrop = GetSimpleBackdrop
Shared.GetOrnateBackdrop = GetOrnateBackdrop
Shared.GetEdgeOnlyBackdrop = GetEdgeOnlyBackdrop
Shared.ApplyBackdrop = ApplyBackdrop
Shared.ApplyOrnateBackdrop = ApplyOrnateBackdrop
Shared.ApplyStyledBackdrop = ApplyStyledBackdrop
Shared.CreateBackdropFrame = CreateBackdropFrame
Shared.CreateLine = CreateLine
Shared.ResolveArtworkColor = ResolveArtworkColor
Shared.ClampTextureSubLevel = ClampTextureSubLevel
Shared.CreateThemeAsset = CreateThemeAsset
Shared.ThemeAssetPath = ThemeAssetPath
Shared.SceneAssetPath = SceneAssetPath
Shared.ConfigureThemeAsset = ConfigureThemeAsset
Shared.GetBackgroundSceneProfile = GetBackgroundSceneProfile
Shared.GetBackgroundSceneColor = GetBackgroundSceneColor
Shared.StopSceneTexture = StopSceneTexture
Shared.ConfigureSceneDrift = ConfigureSceneDrift
Shared.ConfigureScenePulse = ConfigureScenePulse
Shared.ConfigureSceneParticle = ConfigureSceneParticle
