local addonName, NS = ...

-- ============================================================================
-- VAULTSKINS: REVEAL ENGINE
-- ============================================================================
local Skin = NS.Skin
local Shared = NS.SkinShared
local ClampTextureSubLevel = Shared.ClampTextureSubLevel
local RecordStabilityMetric = Shared.RecordStabilityMetric
local ScheduleOwnedTimer = Shared.ScheduleOwnedTimer
local SetColor = Shared.SetColor
local SetVertexColor = Shared.SetVertexColor
local WHITE_TEXTURE = Shared.WHITE_TEXTURE
local ApplyBackdrop = Shared.ApplyBackdrop
local BlendColor = Shared.BlendColor
local CancelOwnedTimer = Shared.CancelOwnedTimer
local EnsureReadableColor = Shared.EnsureReadableColor
local RequestIncrementalMemoryCleanup = Shared.RequestIncrementalMemoryCleanup
local SafeCall = Shared.SafeCall
local REVEAL_ERROR_RECOVERY_TIMER = Shared.REVEAL_ERROR_RECOVERY_TIMER
local REVEAL_SAFETY_DELAYS = Shared.REVEAL_SAFETY_DELAYS
local REVEAL_SAFETY_TIMER = Shared.REVEAL_SAFETY_TIMER
local REVEAL_WATCHDOG_TIMER = Shared.REVEAL_WATCHDOG_TIMER


local REVEAL_GROUP = "vault-reveal"
local REVEAL_POST_GROUP = "vault-reveal-post"
local SOUND_PREVIEW_GROUP = "vault-sound-preview"

-- ======================= SCHEDULER AND MEMORY CONTROL =======================
local function ScheduleCall(group, key, delay, callback, arg1, arg2, arg3, arg4, arg5, arg6)
    if NS.Stability and NS.Stability.ScheduleCall then
        return NS.Stability:ScheduleCall(key, delay, callback, arg1, arg2, arg3, arg4, arg5, arg6, group)
    end
    return ScheduleOwnedTimer(key, delay, function()
        callback(arg1, arg2, arg3, arg4, arg5, arg6)
    end)
end

local function CancelGroup(group, reason)
    if NS.Stability and NS.Stability.CancelGroup then
        return NS.Stability:CancelGroup(group, reason)
    end
    return 0
end

local function SetRevealGC(enabled)
    if NS.Stability and NS.Stability.SetIncrementalGC then
        NS.Stability:SetIncrementalGC(enabled, 0.18, 256)
    end
end

-- ========================== REVEAL TIMING PROFILES ==========================
local MOUNT_SPARK_OFFSETS = {
    { 68, 0 }, { 34, 48 }, { -34, 48 }, { -68, 0 }, { -34, -48 }, { 34, -48 },
}
local TIER_SPARK_OFFSETS = { { 42, 0 }, { 0, 42 }, { -42, 0 }, { 0, -42 } }

local SPEED_PRESETS = {
    fast = {
        startDelay = 0.07,
        stepDelay = 0.11,
        rowDelay = 0.22,
        rewardGap = 0.12,
        fadeDuration = 0.18,
        borderRise = 0.07,
        borderHold = 0.04,
        borderFall = 0.20,
    },
    normal = {
        startDelay = 0.16,
        stepDelay = 0.24,
        rowDelay = 0.48,
        rewardGap = 0.24,
        fadeDuration = 0.34,
        borderRise = 0.11,
        borderHold = 0.09,
        borderFall = 0.38,
    },
    cinematic = {
        startDelay = 0.26,
        stepDelay = 0.38,
        rowDelay = 0.72,
        rewardGap = 0.36,
        fadeDuration = 0.52,
        borderRise = 0.16,
        borderHold = 0.14,
        borderFall = 0.56,
    },
}

local REVEAL_STYLE_PROFILES = {
    fast = {
        effect = "fade",
        pacingScale = 1.00,
        fadeScale = 0.82,
        useBorder = false,
        useFlash = false,
        allowSpecialEffects = false,
    },
    standard = {
        effect = "soft",
        pacingScale = 1.05,
        allowSpecialEffects = false,
        rewardFlashScale = 0.72,
        lockedFlashScale = 0.24,
        rewardBorderScale = 0.78,
        lockedBorderScale = 0.46,
        fadeScale = 1.15,
    },
    pulse = {
        effect = "pulse",
        pacingScale = 1.15,
        allowSpecialEffects = false,
        rewardFlashScale = 0,
        lockedFlashScale = 0,
        rewardBorderScale = 1.28,
        lockedBorderScale = 0.92,
        fadeScale = 0.95,
        useFlash = false,
        secondPulse = true,
        extraDuration = 0.58,
    },
    spotlight = {
        effect = "spotlight",
        pacingScale = 1.25,
        allowSpecialEffects = false,
        rewardFlashScale = 1.38,
        lockedFlashScale = 0,
        rewardBorderScale = 1.22,
        lockedBorderScale = 0,
        fadeScale = 1.20,
        rewardOnlyEffects = true,
        rewardHoldBonus = 0.22,
        extraDuration = 0.42,
    },
    cinematic = {
        effect = "cinematic",
        pacingScale = 1.10,
        allowSpecialEffects = false,
        rewardFlashScale = 1.28,
        lockedFlashScale = 0.52,
        rewardBorderScale = 1.18,
        lockedBorderScale = 0.78,
        fadeScale = 1.25,
        rewardHoldBonus = 0.20,
        secondPulse = true,
        secondFlash = true,
        extraDuration = 0.90,
    },
}

local function GetRevealStyleProfile()
    local style = VaultSkinsDB and VaultSkinsDB.animationPreset or "standard"
    return REVEAL_STYLE_PROFILES[style] or REVEAL_STYLE_PROFILES.standard
end

-- =========================== AUDIO CONFIGURATION ============================
local DEFAULT_SOUND_PROFILE = {
    frameOpen = {
        "UI_WEEKLY_REWARD_OPEN",
        "UI_WEEKLY_REWARDS_OPEN",
        "UI_BONUS_LOOT_ROLL_START",
        "IG_MAINMENU_OPEN",
    },
    open = {
        "UI_WEEKLY_REWARD_OPEN",
        "UI_WEEKLY_REWARDS_OPEN",
        "UI_BONUS_LOOT_ROLL_START",
        "IG_MAINMENU_OPEN",
    },
    crest = {
        "UI_70_ARTIFACT_FORGE_APPEARANCE_LOCKED",
        "IG_MAINMENU_OPTION_CHECKBOX_ON",
    },
    border = {
        "UI_BONUS_LOOT_ROLL_START",
        "IG_MAINMENU_OPTION_CHECKBOX_ON",
    },
    corner = {
        "LOOT_WINDOW_COIN_SOUND",
        "IG_MAINMENU_OPTION_CHECKBOX_ON",
    },
    frameComplete = {
        "UI_BONUS_LOOT_ROLL_END",
        "UI_70_ARTIFACT_FORGE_TRAIT_RANKUP",
    },
    slot = {
        "IG_MAINMENU_OPTION_CHECKBOX_ON",
        "UI_BONUS_LOOT_ROLL_START",
    },
    jackpotTick = {
        "LOOT_WINDOW_COIN_SOUND",
        "IG_MAINMENU_OPTION_CHECKBOX_ON",
    },
    jackpotTickAlt = {
        "IG_MAINMENU_OPTION_CHECKBOX_ON",
        "LOOT_WINDOW_COIN_SOUND",
    },
    jackpotStop = {
        "UI_BONUS_LOOT_ROLL_END",
        "UI_WEEKLY_REWARD_UNLOCK_ITEM",
    },
    jackpotWin = {
        "UI_EPICLOOT_TOAST",
        "UI_70_ARTIFACT_FORGE_TRAIT_FINALRANK",
        "UI_WEEKLY_REWARD_UNLOCK_ITEM",
    },
    clickReveal = {
        "UI_70_ARTIFACT_FORGE_APPEARANCE_LOCKED",
        "UI_BONUS_LOOT_ROLL_END",
        "IG_MAINMENU_OPTION_CHECKBOX_ON",
    },
    rewardQuiet = {
        "IG_MAINMENU_OPTION_CHECKBOX_ON",
        "LOOT_WINDOW_COIN_SOUND",
    },
    rewardNormal = {
        "UI_BONUS_LOOT_ROLL_END",
        "LOOT_WINDOW_COIN_SOUND",
        "IG_MAINMENU_OPTION_CHECKBOX_ON",
    },
    reward = {
        "UI_WEEKLY_REWARD_UNLOCK_ITEM",
        "UI_EPICLOOT_TOAST",
        "UI_BONUS_LOOT_ROLL_END",
        "LOOT_WINDOW_COIN_SOUND",
    },
    rewardImportant = {
        "UI_WEEKLY_REWARD_UNLOCK_ITEM",
        "UI_BONUS_LOOT_ROLL_END",
        "UI_EPICLOOT_TOAST",
    },
    rewardTier = {
        "UI_70_ARTIFACT_FORGE_TRAIT_RANKUP",
        "UI_WEEKLY_REWARD_UNLOCK_ITEM",
        "UI_EPICLOOT_TOAST",
    },
    rewardRare = {
        "UI_70_ARTIFACT_FORGE_TRAIT_FINALRANK",
        "UI_EPICLOOT_TOAST",
        "UI_WEEKLY_REWARD_UNLOCK_ITEM",
    },
    final = {
        "UI_70_ARTIFACT_FORGE_TRAIT_RANKUP",
        "UI_BONUS_LOOT_ROLL_END",
        "IG_MAINMENU_OPTION_CHECKBOX_ON",
    },
    jackpot = {
        "UI_70_ARTIFACT_FORGE_TRAIT_FINALRANK",
        "UI_EPICLOOT_TOAST",
        "UI_BONUS_LOOT_ROLL_END",
    },
}

-- ============================== SOUND ROUTING ===============================
local function GetSpeedPreset()
    local speed = VaultSkinsDB and VaultSkinsDB.revealSpeed or "normal"
    return SPEED_PRESETS[speed] or SPEED_PRESETS.normal
end

local function ResolveSoundKitFromList(candidates)
    if type(candidates) ~= "table" then
        candidates = { candidates }
    end

    for _, candidate in ipairs(candidates) do
        if type(candidate) == "number" then
            return candidate
        elseif type(candidate) == "string" and type(SOUNDKIT) == "table" and SOUNDKIT[candidate] then
            return SOUNDKIT[candidate]
        end
    end
end

local OPENING_SOUND_KINDS = { frameOpen = true, open = true }
local FRAME_DETAIL_SOUND_KINDS = { crest = true, border = true, corner = true, frameComplete = true }
local FRAME_SOUND_KINDS = { frameOpen = true, open = true, crest = true, border = true, corner = true, frameComplete = true }
local REWARD_SOUND_KINDS = {
    slot = true,
    clickReveal = true,
    jackpotTick = true,
    jackpotTickAlt = true,
    jackpotStop = true,
    reward = true,
    rewardImportant = true,
    rewardTier = true,
    rewardRare = true,
    revealQuick = true,
    revealSoft = true,
    revealPulse = true,
    revealSpotlight = true,
    revealCinematic = true,
}
local FINAL_SOUND_KINDS = { final = true, jackpot = true, jackpotWin = true }
local JACKPOT_CASINO_SOUND_KINDS = { jackpotTick = true, jackpotTickAlt = true, jackpotStop = true }
local CUSTOM_JACKPOT_SOUND_FILES = {
    jackpotTick = "Interface\\AddOns\\VaultSkins\\Media\\Sounds\\jackpot_tick.ogg",
    jackpotTickAlt = "Interface\\AddOns\\VaultSkins\\Media\\Sounds\\jackpot_tick_alt.ogg",
    jackpotStop = "Interface\\AddOns\\VaultSkins\\Media\\Sounds\\jackpot_stop.ogg",
    jackpotWin = "Interface\\AddOns\\VaultSkins\\Media\\Sounds\\jackpot_maxwin.ogg",
    jackpotReward = "Interface\\AddOns\\VaultSkins\\Media\\Sounds\\jackpot_reward.ogg",
}
local CUSTOM_REVEAL_SOUND_FILES = {
    off = "Interface\\AddOns\\VaultSkins\\Media\\Sounds\\reveal_quick.ogg",
    fast = "Interface\\AddOns\\VaultSkins\\Media\\Sounds\\reveal_quick.ogg",
    standard = "Interface\\AddOns\\VaultSkins\\Media\\Sounds\\reveal_soft.ogg",
    pulse = "Interface\\AddOns\\VaultSkins\\Media\\Sounds\\reveal_pulse.ogg",
    spotlight = "Interface\\AddOns\\VaultSkins\\Media\\Sounds\\reveal_spotlight.ogg",
    cinematic = "Interface\\AddOns\\VaultSkins\\Media\\Sounds\\reveal_cinematic.ogg",
}
local DIRECT_CUSTOM_SOUND_FILES = {
    clickReveal = "Interface\\AddOns\\VaultSkins\\Media\\Sounds\\click_unlock.ogg",
    revealQuick = CUSTOM_REVEAL_SOUND_FILES.fast,
    revealSoft = CUSTOM_REVEAL_SOUND_FILES.standard,
    revealPulse = CUSTOM_REVEAL_SOUND_FILES.pulse,
    revealSpotlight = CUSTOM_REVEAL_SOUND_FILES.spotlight,
    revealCinematic = CUSTOM_REVEAL_SOUND_FILES.cinematic,
    final = "Interface\\AddOns\\VaultSkins\\Media\\Sounds\\final_flourish.ogg",
    jackpot = CUSTOM_JACKPOT_SOUND_FILES.jackpotWin,
}

local CUSTOM_ONLY_SOUND_KINDS = {
    clickReveal = true,
    jackpotTick = true,
    jackpotTickAlt = true,
    jackpotStop = true,
    jackpotWin = true,
    jackpot = true,
    final = true,
    reward = true,
    rewardImportant = true,
    rewardTier = true,
    rewardRare = true,
    revealQuick = true,
    revealSoft = true,
    revealPulse = true,
    revealSpotlight = true,
    revealCinematic = true,
}

local REWARD_SOUND_PRIORITY = { reward = 1, rewardImportant = 2, rewardTier = 3, rewardRare = 4 }

local function GetCustomSoundPath(kind)
    if DIRECT_CUSTOM_SOUND_FILES[kind] then return DIRECT_CUSTOM_SOUND_FILES[kind] end
    if CUSTOM_JACKPOT_SOUND_FILES[kind] then return CUSTOM_JACKPOT_SOUND_FILES[kind] end
    if REWARD_SOUND_PRIORITY and REWARD_SOUND_PRIORITY[kind] then
        if VaultSkinsDB and VaultSkinsDB.revealMode == "jackpot" then
            return CUSTOM_JACKPOT_SOUND_FILES.jackpotReward
        end
        local style = VaultSkinsDB and VaultSkinsDB.animationPreset or "standard"
        return CUSTOM_REVEAL_SOUND_FILES[style] or CUSTOM_REVEAL_SOUND_FILES.standard
    end
    return nil
end

local function SoundKindEnabled(kind)
    if not (VaultSkinsDB and VaultSkinsDB.sounds) then return false end
    if OPENING_SOUND_KINDS[kind] and VaultSkinsDB.openingSounds == false then return false end
    if FRAME_DETAIL_SOUND_KINDS[kind] and VaultSkinsDB.frameDetailSounds == false then return false end
    if FRAME_SOUND_KINDS[kind] and VaultSkinsDB.frameSounds == false then return false end
    if REWARD_SOUND_KINDS[kind] and VaultSkinsDB.rewardSounds == false then return false end
    if FINAL_SOUND_KINDS[kind] and VaultSkinsDB.finalSounds == false then return false end
    return true
end

local function SoundAllowedByIntensity(kind)
    local intensity = VaultSkinsDB and VaultSkinsDB.soundIntensity or "normal"
    if JACKPOT_CASINO_SOUND_KINDS[kind] then return true end
    if intensity == "quiet" then
        return kind == "frameOpen" or kind == "open" or REWARD_SOUND_KINDS[kind] or kind == "final" or kind == "jackpot"
    end
    if intensity == "normal" then
        return kind ~= "slot" and kind ~= "border" and kind ~= "corner"
    end
    return true
end

local function GetSoundProfileKey(kind)
    local intensity = VaultSkinsDB and VaultSkinsDB.soundIntensity or "normal"
    if not REWARD_SOUND_PRIORITY[kind] then return kind end
    if intensity == "quiet" then
        return kind == "rewardRare" and "rewardNormal" or "rewardQuiet"
    end
    if intensity == "normal" and kind == "reward" then return "rewardNormal" end
    return kind
end

local function RewardSoundReady(kind)
    local intensity = VaultSkinsDB and VaultSkinsDB.soundIntensity or "normal"
    local priority = REWARD_SOUND_PRIORITY[kind] or 1
    local minimumGap = intensity == "quiet" and 0.30 or (intensity == "normal" and 0.10 or 0.05)
    local now = GetTimePreciseSec and GetTimePreciseSec() or (GetTime and GetTime() or 0)
    if Skin.lastRewardSoundAt and now > 0 and now - Skin.lastRewardSoundAt < minimumGap then
        if priority <= (Skin.lastRewardSoundPriority or 1) then
            return false, now, priority
        end
    end
    return true, now, priority
end

-- Reveal audio is isolated from frame-preview sounds to prevent overlap.
local function PlayVaultSound(kind, theme, forceRewardSound)
    if Skin.adaptiveRevealAudioActive and FRAME_SOUND_KINDS[kind] then
        return false
    end
    if not (PlaySound or PlaySoundFile) or not SoundKindEnabled(kind) or not SoundAllowedByIntensity(kind) then return false end
    local rewardReady, rewardTime, rewardPriority = true, nil, nil
    if REWARD_SOUND_PRIORITY[kind] then
        if forceRewardSound then
            rewardTime = GetTimePreciseSec and GetTimePreciseSec() or (GetTime and GetTime() or 0)
            rewardPriority = REWARD_SOUND_PRIORITY[kind] or 1
        else
            rewardReady, rewardTime, rewardPriority = RewardSoundReady(kind)
            if not rewardReady then return false end
        end
    end

    local customSoundPath = GetCustomSoundPath(kind)
    if customSoundPath and PlaySoundFile then
        local ok, willPlay = pcall(PlaySoundFile, customSoundPath, "SFX")
        if ok and willPlay ~= false then
            if REWARD_SOUND_PRIORITY[kind] then
                Skin.lastRewardSoundAt = rewardTime
                Skin.lastRewardSoundPriority = rewardPriority or 1
            end
            return true
        end
    end
    if CUSTOM_ONLY_SOUND_KINDS[kind] then
        return false
    end

    local soundKit
    local profileKey = GetSoundProfileKey(kind)
    local themeProfile = theme and theme.soundProfile
    if type(themeProfile) == "table" then
        soundKit = ResolveSoundKitFromList(themeProfile[profileKey])
        if not soundKit and profileKey ~= kind then soundKit = ResolveSoundKitFromList(themeProfile[kind]) end
        if not soundKit and kind == "frameOpen" then soundKit = ResolveSoundKitFromList(themeProfile.open) end
        if not soundKit and kind == "frameComplete" then soundKit = ResolveSoundKitFromList(themeProfile.final) end
    end
    if not soundKit then
        soundKit = ResolveSoundKitFromList(DEFAULT_SOUND_PROFILE[profileKey])
        if not soundKit and profileKey ~= kind then soundKit = ResolveSoundKitFromList(DEFAULT_SOUND_PROFILE[kind]) end
    end

    if soundKit then
        local ok = pcall(PlaySound, soundKit, "SFX")
        if ok and REWARD_SOUND_PRIORITY[kind] then
            Skin.lastRewardSoundAt = rewardTime
            Skin.lastRewardSoundPriority = rewardPriority or 1
        end
        return ok
    end
    return false
end

local function ScheduleVaultSound(kind, theme, delay, token)
    if not (VaultSkinsDB and VaultSkinsDB.soundSync) then return end
    ScheduleOwnedTimer(nil, math.max(0, tonumber(delay) or 0), function()
        if token and token ~= Skin.animationToken then return end
        if WeeklyRewardsFrame and not WeeklyRewardsFrame:IsShown() then return end
        PlayVaultSound(kind, theme)
    end)
end

-- ========================= REWARD EFFECT ANIMATIONS =========================
local function CreatePulseAnimation(region, peakAlpha, rise, hold, fall, finalAlpha, hideWhenFinished)
    if region.VaultSkinsPulse then
        local group = region.VaultSkinsPulse
        RecordStabilityMetric("animationGroupsReused")
        if group.IsPlaying and group:IsPlaying() then group:Stop() end
        local peak = peakAlpha or 0.8
        group.FinalAlpha = finalAlpha or 0
        group.HideWhenFinished = hideWhenFinished and true or false
        if group.FadeIn then
            group.FadeIn:SetFromAlpha(0)
            group.FadeIn:SetToAlpha(peak)
            group.FadeIn:SetDuration(rise or 0.14)
        end
        if group.Linger then
            group.Linger:SetFromAlpha(peak)
            group.Linger:SetToAlpha(peak)
            group.Linger:SetDuration(hold or 0.10)
        end
        if group.FadeOut then
            group.FadeOut:SetFromAlpha(peak)
            group.FadeOut:SetToAlpha(group.FinalAlpha)
            group.FadeOut:SetDuration(fall or 0.28)
        end
        return group
    end

    local group = region:CreateAnimationGroup()
    RecordStabilityMetric("animationGroupsCreated")
    region.VaultSkinsPulse = group
    group.FinalAlpha = finalAlpha or 0
    group.HideWhenFinished = hideWhenFinished and true or false

    local fadeIn = group:CreateAnimation("Alpha")
    group.FadeIn = fadeIn
    fadeIn:SetFromAlpha(0)
    fadeIn:SetToAlpha(peakAlpha or 0.8)
    fadeIn:SetDuration(rise or 0.14)
    fadeIn:SetOrder(1)

    local linger = group:CreateAnimation("Alpha")
    group.Linger = linger
    linger:SetFromAlpha(peakAlpha or 0.8)
    linger:SetToAlpha(peakAlpha or 0.8)
    linger:SetDuration(hold or 0.10)
    linger:SetOrder(2)

    local fadeOut = group:CreateAnimation("Alpha")
    group.FadeOut = fadeOut
    fadeOut:SetFromAlpha(peakAlpha or 0.8)
    fadeOut:SetToAlpha(group.FinalAlpha)
    fadeOut:SetDuration(fall or 0.28)
    fadeOut:SetOrder(3)

    local function Finish()
        region:SetAlpha(group.FinalAlpha or 0)
        if group.HideWhenFinished then
            region:Hide()
        else
            region:Show()
        end
    end

    group:SetScript("OnPlay", function()
        region:Show()
        region:SetAlpha(0)
    end)
    group:SetScript("OnFinished", Finish)
    group:SetScript("OnStop", Finish)

    return group
end

local function ConfigureRewardSparkBurst(texture, offsetX, offsetY, duration, peakAlpha)
    if not texture then return nil end
    local group = texture.VaultSkinsRewardBurst
    local created = false
    if not group then
        group = texture:CreateAnimationGroup()
        created = true
        texture.VaultSkinsRewardBurst = group
        group.FadeIn = group:CreateAnimation("Alpha")
        group.FadeIn:SetOrder(1)
        group.Move = group:CreateAnimation("Translation")
        group.Move:SetOrder(1)
        group.FadeOut = group:CreateAnimation("Alpha")
        group.FadeOut:SetOrder(2)
        group:SetScript("OnPlay", function() texture:Show(); texture:SetAlpha(0) end)
        local function Finish() texture:SetAlpha(0); texture:Hide() end
        group:SetScript("OnFinished", Finish)
        group:SetScript("OnStop", Finish)
    end
    RecordStabilityMetric(created and "animationGroupsCreated" or "animationGroupsReused")
    if group.IsPlaying and group:IsPlaying() then group:Stop() end
    group.FadeIn:SetFromAlpha(0)
    group.FadeIn:SetToAlpha(peakAlpha or 0.8)
    group.FadeIn:SetDuration(math.max(0.06, (duration or 0.42) * 0.24))
    group.Move:SetOffset(offsetX or 0, offsetY or 0)
    group.Move:SetDuration(duration or 0.42)
    group.FadeOut:SetFromAlpha(peakAlpha or 0.8)
    group.FadeOut:SetToAlpha(0)
    group.FadeOut:SetDuration(math.max(0.10, (duration or 0.42) * 0.48))
    return group
end

-- ========================== SPECIAL REWARD EFFECTS ==========================
function Skin:EnsureSpecialRewardVisuals(activity, skin)
    if not activity or not skin then return false end
    if skin.SpecialRing and skin.TierBeamHorizontal and skin.TierBeamVertical and skin.SpecialSparkles then
        return true
    end

    skin.SpecialRing = activity:CreateTexture(nil, "OVERLAY", nil, ClampTextureSubLevel(7))
    skin.SpecialRing:SetTexture("Interface\\Buttons\\UI-ActionButton-Border")
    skin.SpecialRing:SetBlendMode("ADD")
    skin.SpecialRing:SetPoint("CENTER", activity, "CENTER", 0, -5)
    skin.SpecialRing:SetSize(112, 112)
    skin.SpecialRing:SetAlpha(0)
    skin.SpecialRing:Hide()

    skin.TierBeamHorizontal = activity:CreateTexture(nil, "OVERLAY", nil, 7)
    skin.TierBeamHorizontal:SetTexture(WHITE_TEXTURE)
    skin.TierBeamHorizontal:SetBlendMode("ADD")
    skin.TierBeamHorizontal:SetPoint("LEFT", activity, "LEFT", 8, -5)
    skin.TierBeamHorizontal:SetPoint("RIGHT", activity, "RIGHT", -8, -5)
    skin.TierBeamHorizontal:SetHeight(2)
    skin.TierBeamHorizontal:SetAlpha(0)
    skin.TierBeamHorizontal:Hide()

    skin.TierBeamVertical = activity:CreateTexture(nil, "OVERLAY", nil, 7)
    skin.TierBeamVertical:SetTexture(WHITE_TEXTURE)
    skin.TierBeamVertical:SetBlendMode("ADD")
    skin.TierBeamVertical:SetPoint("TOP", activity, "TOP", 0, -9)
    skin.TierBeamVertical:SetPoint("BOTTOM", activity, "BOTTOM", 0, 9)
    skin.TierBeamVertical:SetWidth(2)
    skin.TierBeamVertical:SetAlpha(0)
    skin.TierBeamVertical:Hide()

    skin.SpecialSparkles = {}
    for index = 1, 6 do
        local sparkle = activity:CreateTexture(nil, "OVERLAY", nil, ClampTextureSubLevel(7))
        sparkle:SetTexture("Interface\\Cooldown\\star4")
        sparkle:SetBlendMode("ADD")
        sparkle:SetPoint("CENTER", activity, "CENTER", 0, -5)
        sparkle:SetSize(index % 2 == 0 and 13 or 10, index % 2 == 0 and 13 or 10)
        sparkle:SetAlpha(0)
        sparkle:Hide()
        skin.SpecialSparkles[index] = sparkle
    end
    RecordStabilityMetric("lazySpecialVisualBuilds")
    return true
end

local function StopRegionPulse(region)
    if region and region.VaultSkinsPulse and region.VaultSkinsPulse:IsPlaying() then
        region.VaultSkinsPulse:Stop()
    end
end

local function StopSpecialRewardAnimations(skin)
    if not skin then return end
    StopRegionPulse(skin.SpecialRing)
    StopRegionPulse(skin.TierBeamHorizontal)
    StopRegionPulse(skin.TierBeamVertical)
    StopRegionPulse(skin.RarityHalo)
    for _, sparkle in ipairs(skin.SpecialSparkles or {}) do
        if sparkle.VaultSkinsRewardBurst and sparkle.VaultSkinsRewardBurst:IsPlaying() then sparkle.VaultSkinsRewardBurst:Stop() end
        sparkle:SetAlpha(0)
        sparkle:Hide()
    end
    if skin.SpecialRing then skin.SpecialRing:SetAlpha(0); skin.SpecialRing:Hide() end
    if skin.TierBeamHorizontal then skin.TierBeamHorizontal:SetAlpha(0); skin.TierBeamHorizontal:Hide() end
    if skin.TierBeamVertical then skin.TierBeamVertical:SetAlpha(0); skin.TierBeamVertical:Hide() end
end

local function TierEchoEvent(activity, color, token)
    if token ~= Skin.animationToken or not activity or not activity:IsShown() then return end
    local currentSkin = activity.VaultSkins
    if currentSkin and currentSkin.SpecialRing then
        SetVertexColor(currentSkin.SpecialRing, color)
        CreatePulseAnimation(currentSkin.SpecialRing, 0.68, 0.08, 0.02, 0.30, 0, true):Play()
    end
end

function Skin:PlaySpecialRewardEffect(activity, profile, theme, token)
    if self.sessionDisabledFeatures and self.sessionDisabledFeatures.rewardRarity then return end
    if not (activity and profile and profile.hasReward and VaultSkinsDB and VaultSkinsDB.rewardSpecialAnimations ~= false) then return end
    if not (profile.isTier or profile.isRare or profile.isMount) then return end

    local skin = self:CreateActivitySkin(activity)
    if not skin then return end
    if not self:EnsureSpecialRewardVisuals(activity, skin) then return end
    StopSpecialRewardAnimations(skin)
    local color = profile.effectColor or theme.accent
    SetVertexColor(skin.SpecialRing, color)
    SetVertexColor(skin.RarityHalo, color)

    if profile.isMount or profile.isRare then
        skin.SpecialRing:SetSize(profile.isMount and 158 or 144, profile.isMount and 158 or 144)
        CreatePulseAnimation(skin.RarityHalo, profile.isMount and 0.52 or 0.44, 0.12, 0.10, 0.62, 0, false):Play()
        CreatePulseAnimation(skin.SpecialRing, 1.00, 0.12, 0.10, 0.56, 0, true):Play()
        local offsets = MOUNT_SPARK_OFFSETS
        for index, sparkle in ipairs(skin.SpecialSparkles or {}) do
            SetVertexColor(sparkle, index % 2 == 0 and color or (theme.silver or color))
            local offset = offsets[index]
            local group = ConfigureRewardSparkBurst(sparkle, offset[1], offset[2], 0.62 + index * 0.025, index % 2 == 0 and 0.92 or 0.72)
            if group then group:Play() end
        end
    elseif profile.isTier then
        skin.SpecialRing:SetSize(116, 116)
        SetColor(skin.TierBeamHorizontal, color)
        SetColor(skin.TierBeamVertical, color)
        CreatePulseAnimation(skin.SpecialRing, 0.90, 0.10, 0.06, 0.34, 0, true):Play()
        CreatePulseAnimation(skin.TierBeamHorizontal, 0.62, 0.08, 0.04, 0.28, 0, true):Play()
        CreatePulseAnimation(skin.TierBeamVertical, 0.48, 0.08, 0.04, 0.32, 0, true):Play()
        local offsets = TIER_SPARK_OFFSETS
        for index = 1, 4 do
            local sparkle = skin.SpecialSparkles[index]
            SetVertexColor(sparkle, index % 2 == 0 and (theme.silver or color) or color)
            local offset = offsets[index]
            local group = ConfigureRewardSparkBurst(sparkle, offset[1], offset[2], 0.46 + index * 0.02, 0.78)
            if group then group:Play() end
        end
        ScheduleCall(REVEAL_GROUP, nil, 0.46, TierEchoEvent, activity, color, token)
    end
end


Shared.CreatePulseAnimation = CreatePulseAnimation
Shared.PlayVaultSound = PlayVaultSound
Shared.ScheduleVaultSound = ScheduleVaultSound
Shared.StopSpecialRewardAnimations = StopSpecialRewardAnimations
Shared.CUSTOM_JACKPOT_SOUND_FILES = CUSTOM_JACKPOT_SOUND_FILES

local function FrameControlEnabled(key)
    return not VaultSkinsDB or VaultSkinsDB[key] ~= false
end

-- ========================== OPENING SESSION STATE ===========================
local Engine = {}
NS.RevealEngine = Engine

local session = {
    token = 0,
    visible = {},
    work = {},
    scheduleActivities = {},
    scheduleDelays = {},
    scheduleSounds = {},
    jackpotSequence = {},
    revealed = {},
    preset = {},
    scheduleCount = 0,
    jackpotCount = 0,
    revealFinished = false,
}
Engine.session = session

-- ========================= REVEAL SCHEDULE BUILDING =========================
local function ClearArray(array)
    for index = #array, 1, -1 do array[index] = nil end
end

local function ClearMap(map)
    for key in pairs(map) do map[key] = nil end
end

local function ReleaseSessionReferences(keepFrame)
    ClearArray(session.visible)
    ClearArray(session.work)
    ClearArray(session.scheduleActivities)
    ClearArray(session.scheduleDelays)
    ClearArray(session.scheduleSounds)
    ClearArray(session.jackpotSequence)
    ClearMap(session.revealed)
    session.scheduleCount = 0
    session.jackpotCount = 0
    session.theme = nil
    session.rewardSummary = nil
    session.revealStyle = nil
    session.animationStyle = nil
    session.jackpotTarget = nil
    session.rewardCount = 0
    session.remaining = 0
    session.latestDelay = 0
    session.revealMode = nil
    if not keepFrame then session.frame = nil end
end

local function SortActivitiesVisually(activities)
    table.sort(activities, function(first, second)
        local firstTop = first:GetTop() or 0
        local secondTop = second:GetTop() or 0
        if math.abs(firstTop - secondTop) > 4 then return firstTop > secondTop end
        return (first:GetLeft() or 0) < (second:GetLeft() or 0)
    end)
end

local function CopyActivities(source, destination)
    ClearArray(destination)
    for index = 1, #source do destination[index] = source[index] end
    return destination
end

local function ShuffleActivities(source, destination)
    CopyActivities(source, destination)
    for index = #destination, 2, -1 do
        local swapIndex = math.random(index)
        destination[index], destination[swapIndex] = destination[swapIndex], destination[index]
    end
    return destination
end

local function PickJackpotTarget(activities)
    local rewardCount = 0
    for index = 1, #activities do
        if activities[index] and activities[index].hasRewards then rewardCount = rewardCount + 1 end
    end
    if rewardCount > 0 then
        local wanted = math.random(rewardCount)
        for index = 1, #activities do
            local activity = activities[index]
            if activity and activity.hasRewards then
                wanted = wanted - 1
                if wanted == 0 then return activity end
            end
        end
    end
    if #activities > 0 then return activities[math.random(#activities)] end
end

local function AddSchedule(activity, delay, soundKind)
    local index = session.scheduleCount + 1
    session.scheduleCount = index
    session.scheduleActivities[index] = activity
    session.scheduleDelays[index] = delay
    session.scheduleSounds[index] = soundKind
end

local function BuildRevealSchedule(preset, mode, jackpotTarget, jackpotStartDelay)
    ClearArray(session.scheduleActivities)
    ClearArray(session.scheduleDelays)
    ClearArray(session.scheduleSounds)
    session.scheduleCount = 0
    local visual = session.visible

    if mode == "click" then return end

    if mode == "jackpot" then
        ClearArray(session.work)
        for index = 1, #visual do
            local activity = visual[index]
            if activity ~= jackpotTarget then session.work[#session.work + 1] = activity end
        end
        for index = #session.work, 2, -1 do
            local swapIndex = math.random(index)
            session.work[index], session.work[swapIndex] = session.work[swapIndex], session.work[index]
        end
        if jackpotTarget then session.work[#session.work + 1] = jackpotTarget end
        local startDelay = tonumber(jackpotStartDelay) or (preset.startDelay + 2.40)
        local stepDelay = math.max(0.34, (preset.stepDelay or 0.16) * 1.60)
        for index = 1, #session.work do
            local activity = session.work[index]
            AddSchedule(activity, startDelay + ((index - 1) * stepDelay), activity.hasRewards and "reward" or "slot")
        end
        return
    end

    if mode == "rows" then
        local lastTop
        local rowIndex = 0
        local rowStart = 1
        for index = 1, #visual + 1 do
            local activity = visual[index]
            local top = activity and (activity:GetTop() or 0) or nil
            if index == #visual + 1 or lastTop == nil or math.abs(lastTop - top) > 4 then
                if lastTop ~= nil then
                    rowIndex = rowIndex + 1
                    local delay = preset.startDelay + ((rowIndex - 1) * preset.rowDelay)
                    local soundActivity = visual[rowStart]
                    local rowSound = "slot"
                    for rowItem = rowStart, index - 1 do
                        if visual[rowItem].hasRewards then
                            soundActivity = visual[rowItem]
                            rowSound = "reward"
                            break
                        end
                    end
                    for rowItem = rowStart, index - 1 do
                        local rowActivity = visual[rowItem]
                        AddSchedule(rowActivity, delay, rowActivity == soundActivity and rowSound or nil)
                    end
                end
                rowStart = index
                lastTop = top
            end
        end
        return
    end

    if mode == "random" then
        ShuffleActivities(visual, session.work)
        for index = 1, #session.work do
            local activity = session.work[index]
            AddSchedule(activity, preset.startDelay + ((index - 1) * preset.stepDelay), activity.hasRewards and "reward" or "slot")
        end
        return
    end

    if mode == "rewardslast" then
        local orderedIndex = 0
        ClearArray(session.work)
        for index = 1, #visual do
            local activity = visual[index]
            if not activity.hasRewards then orderedIndex = orderedIndex + 1; session.work[orderedIndex] = activity end
        end
        local firstRewardIndex = orderedIndex + 1
        for index = 1, #visual do
            local activity = visual[index]
            if activity.hasRewards then orderedIndex = orderedIndex + 1; session.work[orderedIndex] = activity end
        end
        for index = 1, orderedIndex do
            local activity = session.work[index]
            local delay = preset.startDelay + ((index - 1) * preset.stepDelay)
            if index >= firstRewardIndex and firstRewardIndex > 1 then delay = delay + preset.rewardGap end
            AddSchedule(activity, delay, activity.hasRewards and "reward" or "slot")
        end
        return
    end

    for index = 1, #visual do
        local activity = visual[index]
        AddSchedule(activity, preset.startDelay + ((index - 1) * preset.stepDelay), activity.hasRewards and "reward" or "slot")
    end
end

local CoverOnClick
local CoverOnEnter
local CoverOnLeave

-- ====================== CARD COVER AND REVEAL VISUALS =======================
local function EnsureRevealCover(activity, theme)
    local skin = Skin:CreateActivitySkin(activity)
    if not skin then return nil end
    local coverParent = activity:GetParent() or WeeklyRewardsFrame or UIParent
    local cover = skin.RevealCover
    if not cover then
        cover = CreateFrame("Button", nil, coverParent, "BackdropTemplate")
        skin.RevealCover = cover
        cover:RegisterForClicks("LeftButtonUp")
        cover.Text = cover:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        cover.Text:SetPoint("CENTER", 0, 0)
        cover.Text:SetJustifyH("CENTER")
        local font, size, flags = cover.Text:GetFont()
        cover.DefaultFont = { font = font, size = size, flags = flags }
        cover.Accent = cover:CreateTexture(nil, "OVERLAY")
        cover.Accent:SetTexture(WHITE_TEXTURE)
        cover.Accent:SetPoint("BOTTOMLEFT", 10, 7)
        cover.Accent:SetPoint("BOTTOMRIGHT", -10, 7)
        cover.Accent:SetHeight(2)
        cover.VaultSkinsBackground = {}
        cover.VaultSkinsBorder = {}
        cover:SetScript("OnClick", CoverOnClick)
        cover:SetScript("OnEnter", CoverOnEnter)
        cover:SetScript("OnLeave", CoverOnLeave)
    elseif cover:GetParent() ~= coverParent then
        cover:SetParent(coverParent)
    end

    cover.VaultSkinsActivity = activity
    cover.VaultSkinsTheme = theme
    cover:ClearAllPoints()
    cover:SetAllPoints(activity)
    if activity.GetFrameStrata and cover.SetFrameStrata then cover:SetFrameStrata(activity:GetFrameStrata() or "HIGH") end
    cover:SetFrameLevel(math.max(1, (activity:GetFrameLevel() or 1) + 200))

    local background = cover.VaultSkinsBackground
    local first, second, amount = theme.background, theme.panel, 0.74
    background.r = first.r + (second.r - first.r) * amount
    background.g = first.g + (second.g - first.g) * amount
    background.b = first.b + (second.b - first.b) * amount
    background.a = 1
    local border = cover.VaultSkinsBorder
    border.r, border.g, border.b, border.a = theme.border.r, theme.border.g, theme.border.b, 1
    ApplyBackdrop(cover, background, border, 2)
    local textColor = EnsureReadableColor(theme.text, 0.84)
    cover.Text:SetTextColor(textColor.r, textColor.g, textColor.b, 1)
    cover.Accent:SetColorTexture(theme.accent.r, theme.accent.g, theme.accent.b, 0.78)
    cover:SetAlpha(1)
    cover:Show()
    return cover
end

local function HideRevealCover(activity)
    local skin = activity and activity.VaultSkins
    local cover = skin and skin.RevealCover
    if cover then
        cover.VaultSkinsClickEnabled = false
        cover:EnableMouse(false)
        cover:Hide()
    end
end

local function ConfigureClickRevealCover(activity, theme)
    local cover = EnsureRevealCover(activity, theme)
    if not cover then return nil end
    cover.Text:SetText("CLICK TO REVEAL")
    if cover.DefaultFont and cover.DefaultFont.font then
        cover.Text:SetFont(cover.DefaultFont.font, cover.DefaultFont.size or 11, cover.DefaultFont.flags or "OUTLINE")
    end
    cover.VaultSkinsClickEnabled = true
    cover:EnableMouse(true)
    return cover
end

local function ConfigureJackpotRevealCover(activity, theme)
    local cover = EnsureRevealCover(activity, theme)
    if not cover then return nil end
    cover.Text:SetText("?")
    local font, _, flags = cover.Text:GetFont()
    if font then cover.Text:SetFont(font, 24, flags or "OUTLINE") end
    cover.VaultSkinsClickEnabled = false
    cover:EnableMouse(false)
    return cover
end

CoverOnEnter = function(cover)
    local theme = cover.VaultSkinsTheme
    if theme and cover.VaultSkinsClickEnabled then
        cover:SetBackdropBorderColor(theme.accent.r, theme.accent.g, theme.accent.b, 1)
        cover.Accent:SetAlpha(1)
    end
end

CoverOnLeave = function(cover)
    local theme = cover.VaultSkinsTheme
    if theme then
        cover:SetBackdropBorderColor(theme.border.r, theme.border.g, theme.border.b, 0.95)
        cover.Accent:SetAlpha(0.78)
    end
end

local function CreateCardRevealAnimation(activity)
    local skin = Skin:CreateActivitySkin(activity)
    if not skin then return nil end
    if skin.CardRevealGroup then
        RecordStabilityMetric("animationGroupsReused")
        if skin.CardRevealGroup:IsPlaying() then skin.CardRevealGroup:Stop() end
        return skin.CardRevealGroup
    end
    local group = activity:CreateAnimationGroup()
    RecordStabilityMetric("animationGroupsCreated")
    skin.CardRevealGroup = group
    local fade = group:CreateAnimation("Alpha")
    skin.CardRevealAlpha = fade
    fade:SetFromAlpha(0)
    fade:SetToAlpha(1)
    fade:SetDuration(0.22)
    fade:SetOrder(1)
    group:SetScript("OnPlay", function() activity:SetAlpha(0); activity:Show() end)
    local function Finish() activity:SetAlpha(1) end
    group:SetScript("OnFinished", Finish)
    group:SetScript("OnStop", Finish)
    return group
end

local function StopPulse(region)
    local group = region and region.VaultSkinsPulse
    if group and group.IsPlaying and group:IsPlaying() then group:Stop() end
end

local function PostEnableAudio(current)
    if current.token == Skin.animationToken then Skin.adaptiveRevealAudioActive = false end
end

local function PostStartIdle(current)
    local frame = current.frame
    if current.token == Skin.animationToken and frame and frame:IsShown() and not Skin.revealActive then
        SafeCall("frame idle animation", Skin.StartFrameIdleAnimation, Skin)
    end
end

local function PostRestore(current)
    local frame = current.frame
    if current.token == Skin.animationToken and frame and frame:IsShown() and not Skin.revealActive then
        SafeCall("post reveal frame restore", Skin.RefreshFrameAnimation, Skin)
        if type(collectgarbage) == "function" then collectgarbage("step", 1024) end
    end
    SetRevealGC(false)
    ReleaseSessionReferences(false)
end

local function FinalFlourishEcho(frame, mainSkin, color, peak, jackpotLevel, token)
    if token ~= Skin.animationToken or not frame or not frame:IsShown() then return end
    mainSkin.OpeningGlow:SetBackdropBorderColor(color.r, color.g, color.b, 1)
    CreatePulseAnimation(mainSkin.OpeningGlow, math.min(1, peak * 0.82), 0.08, 0.05, 0.36 + jackpotLevel * 0.06, 0, true):Play()
end

-- ============================== FINAL FLOURISH ==============================
function Skin:PlayFinalFlourish(theme, activities, summary, forceJackpotMode)
    local jackpotLevel = 0
    if VaultSkinsDB and VaultSkinsDB.rewardRarityEffects ~= false and VaultSkinsDB.rewardJackpotEffects ~= false and summary then
        jackpotLevel = tonumber(summary.jackpotLevel) or 0
    end
    if forceJackpotMode and jackpotLevel < 2 then jackpotLevel = 2 end
    if not (VaultSkinsDB and (VaultSkinsDB.finalFlourish or jackpotLevel > 0 or forceJackpotMode)) then return end
    local frame = WeeklyRewardsFrame
    if not frame or not frame:IsShown() then return end

    local mainSkin = self:CreateMainSkin(frame)
    local animationStyle = theme.animationStyle or Shared.EMPTY_TABLE or {}
    local rewardCount = summary and summary.rewardCount or 0
    local importance = summary and summary.maxImportance or 0
    local jackpot = forceJackpotMode == true or jackpotLevel > 0
    local peak = jackpot and math.min(1, (animationStyle.jackpotGlowAlpha or 1.0) * (0.72 + jackpotLevel * 0.14)) or (animationStyle.finalGlowAlpha or 0.62)
    local hold = jackpot and (0.10 + jackpotLevel * 0.07) or 0.08
    local fall = jackpot and (0.42 + jackpotLevel * 0.10) or 0.34
    local flourishColor = theme.accent
    for index = 1, #(activities or Shared.EMPTY_TABLE) do
        local activity = activities[index]
        local profile = self:GetRewardRarityProfile(activity)
        if profile and profile.hasReward and (profile.isTier or profile.isRare or profile.isMount) and (profile.importance or 0) >= importance and profile.effectColor then
            flourishColor = profile.effectColor
            importance = profile.importance or importance
        end
    end

    mainSkin.OpeningGlow:SetBackdropBorderColor(flourishColor.r, flourishColor.g, flourishColor.b, 1)
    CreatePulseAnimation(mainSkin.OpeningGlow, peak, 0.10, hold, fall, 0, true):Play()
    local crestPeak = jackpot and math.min(1, 0.72 + jackpotLevel * 0.10) or 0.72
    local dividerPeak = jackpot and math.min(0.94, 0.52 + jackpotLevel * 0.10) or 0.52
    mainSkin.CrestGlow:SetBackdropBorderColor(flourishColor.r, flourishColor.g, flourishColor.b, 1)
    SetVertexColor(mainSkin.HeaderDividerGlow, flourishColor)
    CreatePulseAnimation(mainSkin.CrestGlow, crestPeak, 0.10, 0.08, 0.40 + jackpotLevel * 0.05, 0, false):Play()
    CreatePulseAnimation(mainSkin.HeaderDividerGlow, dividerPeak, 0.10, 0.05, 0.34 + jackpotLevel * 0.04, 0, false):Play()

    for index = 1, #(activities or Shared.EMPTY_TABLE) do
        local activity = activities[index]
        if activity and activity.hasRewards then
            local skin = self:CreateActivitySkin(activity)
            if skin then
                local profile = self:GetRewardRarityProfile(activity)
                local color = profile and profile.effectColor or theme.accent
                local multiplier = profile and profile.glowMultiplier or 1
                local cardPeak = math.min(1, (jackpot and (0.58 + jackpotLevel * 0.10) or 0.66) * multiplier)
                skin.RevealBorder:SetBackdropBorderColor(color.r, color.g, color.b, 1)
                CreatePulseAnimation(skin.RevealBorder, cardPeak, 0.08, jackpot and (0.06 + jackpotLevel * 0.035) or 0.05, jackpot and (0.30 + jackpotLevel * 0.06) or 0.24, 0, true):Play()
            end
        end
    end

    if jackpotLevel >= 2 then
        ScheduleCall(REVEAL_POST_GROUP, nil, 0.34, FinalFlourishEcho, frame, mainSkin, flourishColor, peak, jackpotLevel, self.animationToken)
    end
    if forceJackpotMode then PlayVaultSound("jackpotWin", theme)
    elseif jackpot then PlayVaultSound("jackpot", theme)
    elseif rewardCount > 0 then PlayVaultSound("final", theme) end
end

local function BorderEcho(current, activity)
    if current.token ~= Skin.animationToken or not current.frame or not current.frame:IsShown() or not activity or not activity:IsShown() then return end
    local skin = activity.VaultSkins
    if not skin or skin.VaultSkinsEchoToken ~= current.token then return end
    local color = skin.VaultSkinsEchoColor
    local alpha = skin.VaultSkinsEchoBorderAlpha or 0.6
    if not color then return end
    skin.RevealBorder:SetBackdropBorderColor(color.r, color.g, color.b, alpha)
    CreatePulseAnimation(skin.RevealBorder, alpha, 0.045, 0.025, 0.18, 0, true):Play()
end

local function FlashEcho(current, activity)
    if current.token ~= Skin.animationToken or not current.frame or not current.frame:IsShown() or not activity or not activity:IsShown() then return end
    local skin = activity.VaultSkins
    if not skin or skin.VaultSkinsEchoToken ~= current.token then return end
    local color = skin.VaultSkinsEchoColor
    local alpha = skin.VaultSkinsEchoFlashAlpha or 0.4
    if not color then return end
    SetColor(skin.RevealFlash, color)
    CreatePulseAnimation(skin.RevealFlash, alpha, 0.07, 0.04, 0.24, 0, true):Play()
end

-- ============================= REVEAL EXECUTION =============================
local function RevealActivity(current, activity, soundKind, forceRewardSound)
    local frame = current.frame
    if current.token ~= Skin.animationToken or not frame or not frame:IsShown() or not activity or not activity:IsShown() then return false end
    HideRevealCover(activity)
    activity:SetAlpha(1)
    current.revealed[activity] = true

    local activitySkin = Skin:CreateActivitySkin(activity)
    if not activitySkin then activity:SetAlpha(1); return true end
    StopSpecialRewardAnimations(activitySkin)
    local rarityProfile = Skin:GetRewardRarityProfile(activity)
    local revealGroup = CreateCardRevealAnimation(activity)
    if not revealGroup then activity:SetAlpha(1); return true end
    local preset, revealStyle, animationStyle = current.preset, current.revealStyle, current.animationStyle
    activitySkin.CardRevealAlpha:SetDuration(preset.fadeDuration * (revealStyle.fadeScale or 1))

    local specialColor = rarityProfile and (rarityProfile.isTier or rarityProfile.isRare or rarityProfile.isMount) and rarityProfile.effectColor or nil
    local revealColor = activity.hasRewards and (specialColor or current.theme.accent) or current.theme.border
    local glowMultiplier = 1
    if rarityProfile and VaultSkinsDB and VaultSkinsDB.rewardItemLevelGlow ~= false then glowMultiplier = rarityProfile.glowMultiplier or 1 end
    local flashScale = activity.hasRewards and (revealStyle.rewardFlashScale or 1) or (revealStyle.lockedFlashScale or 1)
    local borderScale = activity.hasRewards and (revealStyle.rewardBorderScale or 1) or (revealStyle.lockedBorderScale or 1)
    local useBorder = revealStyle.useBorder ~= false
    local useFlash = revealStyle.useFlash ~= false
    if revealStyle.rewardOnlyEffects and not activity.hasRewards then useBorder, useFlash = false, false end
    local flashAlpha = activity.hasRewards and math.min(1, (animationStyle.unlockedFlashAlpha or 0.42) * glowMultiplier * flashScale) or math.min(1, (animationStyle.lockedFlashAlpha or 0.20) * flashScale)
    local borderAlpha = activity.hasRewards and math.min(1, (animationStyle.unlockedBorderAlpha or 1.0) * math.max(0.88, glowMultiplier) * borderScale) or math.min(1, (animationStyle.lockedBorderAlpha or 0.72) * borderScale)
    local importanceHold = rarityProfile and math.max(0, (rarityProfile.importance or 1) - 1) * 0.035 or 0
    if activity.hasRewards then importanceHold = importanceHold + (revealStyle.rewardHoldBonus or 0) end

    activitySkin.RevealBorder:SetBackdropBorderColor(revealColor.r, revealColor.g, revealColor.b, borderAlpha)
    SetColor(activitySkin.RevealFlash, revealColor)
    local borderPulse, flashPulse
    if useBorder then
        borderPulse = CreatePulseAnimation(activitySkin.RevealBorder, borderAlpha, preset.borderRise, preset.borderHold + importanceHold, preset.borderFall + importanceHold, 0, true)
    else
        activitySkin.RevealBorder:SetAlpha(0); activitySkin.RevealBorder:Hide()
    end
    if useFlash then
        flashPulse = CreatePulseAnimation(activitySkin.RevealFlash, flashAlpha, preset.borderRise, 0.02 + importanceHold * 0.40, preset.borderFall + importanceHold, 0, true)
    else
        activitySkin.RevealFlash:SetAlpha(0); activitySkin.RevealFlash:Hide()
    end

    local effectiveSoundKind = soundKind
    if effectiveSoundKind and activity.hasRewards and VaultSkinsDB and VaultSkinsDB.rewardImportanceSounds ~= false and NS.RewardRarity then
        effectiveSoundKind = NS.RewardRarity:GetSoundKind(rarityProfile)
    end
    if effectiveSoundKind then PlayVaultSound(effectiveSoundKind, current.theme, forceRewardSound) end
    revealGroup:Play()
    if borderPulse then borderPulse:Play() end
    if flashPulse then flashPulse:Play() end

    activitySkin.VaultSkinsEchoToken = current.token
    activitySkin.VaultSkinsEchoColor = revealColor
    if revealStyle.secondPulse and useBorder and activity.hasRewards then
        activitySkin.VaultSkinsEchoBorderAlpha = math.min(1, borderAlpha * 0.78)
        local delay = preset.borderRise + preset.borderHold + importanceHold + preset.borderFall + 0.035
        ScheduleCall(REVEAL_GROUP, nil, delay, BorderEcho, current, activity)
    end
    if revealStyle.secondFlash and useFlash and activity.hasRewards then
        activitySkin.VaultSkinsEchoFlashAlpha = math.min(1, flashAlpha * 0.58)
        local delay = preset.borderRise + preset.borderHold + importanceHold + preset.borderFall + 0.12
        ScheduleCall(REVEAL_GROUP, nil, delay, FlashEcho, current, activity)
    end
    if revealStyle.allowSpecialEffects ~= false then Skin:PlaySpecialRewardEffect(activity, rarityProfile, current.theme, current.token) end
    return true
end

local function RevealEvent(current, activity, soundKind, forceRewardSound)
    SafeCall("advanced card reveal", RevealActivity, current, activity, soundKind, forceRewardSound)
end

-- ====================== COMPLETION AND JACKPOT EVENTS =======================
local function FinishReveal(current)
    if current.revealFinished or current.token ~= Skin.animationToken then return end
    current.revealFinished = true
    CancelOwnedTimer(REVEAL_SAFETY_TIMER)
    CancelOwnedTimer(REVEAL_WATCHDOG_TIMER)
    CancelGroup(REVEAL_GROUP, "reveal complete")
    Skin.revealActive = false
    Skin.revealedActivities = nil
    local frame = current.frame
    for index = 1, #current.visible do
        local activity = current.visible[index]
        if activity then HideRevealCover(activity); activity:SetAlpha(1) end
    end
    local summary = current.rewardSummary
    if summary then summary.rewardCount = summary.rewardCount or current.rewardCount end
    SafeCall("final flourish", Skin.PlayFinalFlourish, Skin, current.theme, current.visible, summary, current.revealMode == "jackpot")
    ScheduleCall(REVEAL_POST_GROUP, nil, 1.20, PostEnableAudio, current)
    ScheduleCall(REVEAL_POST_GROUP, nil, 0.72, PostStartIdle, current)
    ScheduleCall(REVEAL_POST_GROUP, nil, 1.50, PostRestore, current)
end

local function FinishEvent(current) FinishReveal(current) end

local function ClickRevealEvent(current, activity)
    if current.token ~= Skin.animationToken or not current.frame or not current.frame:IsShown() then return end
    local hasReward = activity and activity.hasRewards == true
    if RevealActivity(current, activity, hasReward and "reward" or "slot", hasReward) then
        current.remaining = current.remaining - 1
        if current.remaining <= 0 then ScheduleCall(REVEAL_GROUP, nil, 0.24, FinishEvent, current) end
    end
end

CoverOnClick = function(cover)
    if not cover.VaultSkinsClickEnabled then return end
    local activity = cover.VaultSkinsActivity
    if session.token ~= Skin.animationToken or not activity then return end
    cover.VaultSkinsClickEnabled = false
    cover:EnableMouse(false)
    cover:Hide()
    local hasReward = activity.hasRewards == true
    PlayVaultSound("clickReveal", session.theme, true)
    ScheduleCall(REVEAL_GROUP, nil, hasReward and 0.09 or 0.04, ClickRevealEvent, session, activity)
end

local function JackpotStepEvent(current, step)
    local frame = current.frame
    if current.token ~= Skin.animationToken or not frame or not frame:IsShown() then return end
    local activity = current.jackpotSequence[step]
    local previous = current.jackpotPrevious
    if previous and previous ~= activity then
        local oldCover = previous.VaultSkins and previous.VaultSkins.RevealCover
        if oldCover and oldCover:IsShown() then
            oldCover:SetBackdropBorderColor(current.theme.border.r, current.theme.border.g, current.theme.border.b, 0.95)
            oldCover.Accent:SetAlpha(0.45)
        end
    end
    local cover = activity and activity.VaultSkins and activity.VaultSkins.RevealCover
    if cover and cover:IsShown() then
        cover:SetAlpha(1)
        cover:SetBackdropBorderColor(current.theme.accent.r, current.theme.accent.g, current.theme.accent.b, 1)
        cover.Accent:SetAlpha(1)
        local interval = current.jackpotIntervals and current.jackpotIntervals[step] or 0.12
        CreatePulseAnimation(cover.Accent, 1, 0.025, 0.015, math.max(0.045, interval * 0.75), 0.45, false):Play()
    end
    if step == current.jackpotCount then PlayVaultSound("jackpotStop", current.theme)
    else PlayVaultSound(step % 2 == 0 and "jackpotTickAlt" or "jackpotTick", current.theme) end
    current.jackpotPrevious = activity
end

local function PlayJackpotShuffle(current)
    local activities = current.visible
    local count = #activities
    if count == 0 then return 0 end
    local target = PickJackpotTarget(activities)
    current.jackpotTarget = target
    ClearArray(current.jackpotSequence)
    current.jackpotIntervals = current.jackpotIntervals or {}
    ClearArray(current.jackpotIntervals)
    local steps = math.max(11, math.min(18, count * 3))
    local previous
    for step = 1, steps do
        local pickIndex = math.random(count)
        local pick = activities[pickIndex]
        if count > 1 and pick == previous then pickIndex = (pickIndex % count) + 1; pick = activities[pickIndex] end
        current.jackpotSequence[step] = pick
        previous = pick
    end
    if target then steps = steps + 1; current.jackpotSequence[steps] = target end
    current.jackpotCount = steps
    local elapsed = 0.04
    for step = 1, steps do
        local progress = step / steps
        local interval = 0.10 + (progress * progress * 0.34)
        elapsed = elapsed + interval
        current.jackpotIntervals[step] = interval
        ScheduleCall(REVEAL_GROUP, nil, elapsed, JackpotStepEvent, current, step)
    end
    return elapsed + 0.48
end

local function SafetyEvent(current)
    if current.token == Skin.animationToken and Skin.revealActive then Skin:StopOpeningAnimation() end
end

local function WatchdogEvent(current)
    if current.token == Skin.animationToken and Skin.revealActive then SafeCall("animation watchdog", Skin.StopOpeningAnimation, Skin) end
end

local function FrameEffectsEvent(current)
    if current.token ~= Skin.animationToken or not current.frame or not current.frame:IsShown() or not Skin:IsEnabled() then return end
    SafeCall("independent safe frame effects", Skin.PlayFrameOpeningAnimation, Skin, current.theme, current.token)
end

local function StartIdleEvent(frame, token)
    if token == Skin.animationToken and frame and frame:IsShown() then Skin:StartFrameIdleAnimation() end
end

-- ======================= OPENING ANIMATION LIFECYCLE ========================
function Skin:StopOpeningAnimation(suppressIdle)
    CancelOwnedTimer(REVEAL_ERROR_RECOVERY_TIMER)
    CancelOwnedTimer(REVEAL_SAFETY_TIMER)
    CancelOwnedTimer(REVEAL_WATCHDOG_TIMER)
    CancelGroup(REVEAL_GROUP, "reveal stopped")
    CancelGroup(REVEAL_POST_GROUP, "reveal stopped")
    SetRevealGC(false)
    self.animationToken = (self.animationToken or 0) + 1
    self.pendingOpeningRevealPrivacy = false
    self.revealActive = false
    self.adaptiveRevealAudioActive = false
    self.revealedActivities = nil

    local frame = WeeklyRewardsFrame
    if not frame or not frame.VaultSkins then ReleaseSessionReferences(false); return end
    local main = frame.VaultSkins
    StopPulse(main.OpeningGlow); StopPulse(main.TopGlow); StopPulse(main.SideGlowLeft); StopPulse(main.SideGlowRight); StopPulse(main.CrestGlow); StopPulse(main.HeaderDividerGlow)
    if frame.Activities then
        for index = 1, #frame.Activities do
            local activity = frame.Activities[index]
            local skin = activity and activity.VaultSkins
            if activity then activity:SetAlpha(1) end
            if skin then
                if skin.CardRevealGroup and skin.CardRevealGroup:IsPlaying() then skin.CardRevealGroup:Stop() end
                StopPulse(skin.RevealBorder); StopPulse(skin.RevealFlash)
                StopSpecialRewardAnimations(skin)
                if skin.RarityHalo then skin.RarityHalo:SetAlpha(0); skin.RarityHalo:Show() end
                HideRevealCover(activity)
            end
        end
    end
    ReleaseSessionReferences(false)
    if not suppressIdle and frame:IsShown() and self:IsEnabled() then
        ScheduleCall(REVEAL_POST_GROUP, nil, 0, StartIdleEvent, frame, self.animationToken)
    end
end

function Skin:SkipOpeningAnimation() self:StopOpeningAnimation() end

function Skin:PlayOpeningAnimation(force)
    local frame = WeeklyRewardsFrame
    if not frame or not frame:IsShown() or not self:IsEnabled() then return false end
    if not force and not (VaultSkinsDB and VaultSkinsDB.animations) then
        self.pendingOpeningRevealPrivacy = false
        if frame.Activities then for index = 1, #frame.Activities do if frame.Activities[index] then frame.Activities[index]:SetAlpha(1) end end end
        return false
    end
    local now = GetTime and GetTime() or 0
    if not force and not self.pendingOpeningRevealPrivacy and self.lastAutomaticAnimation and now - self.lastAutomaticAnimation < 1.5 then return false end
    if not force then self.lastAutomaticAnimation = now end

    self:StopOpeningAnimation(true)
    local current = session
    current.token = self.animationToken
    current.frame = frame
    current.theme = self:GetTheme()
    current.animationStyle = current.theme.animationStyle or Shared.EMPTY_TABLE or {}
    local basePreset = GetSpeedPreset()
    current.revealStyle = GetRevealStyleProfile()
    local pacing = tonumber(current.revealStyle.pacingScale) or 1
    local preset = current.preset
    preset.startDelay = basePreset.startDelay * pacing
    preset.stepDelay = basePreset.stepDelay * pacing
    preset.rowDelay = basePreset.rowDelay * pacing
    preset.rewardGap = basePreset.rewardGap * pacing
    preset.fadeDuration = basePreset.fadeDuration * pacing
    preset.borderRise = basePreset.borderRise * pacing
    preset.borderHold = basePreset.borderHold * pacing
    preset.borderFall = basePreset.borderFall * pacing
    current.revealFinished = false
    current.jackpotPrevious = nil
    current.rewardCount = 0
    ClearArray(current.visible)
    ClearMap(current.revealed)

    if frame.Activities then
        for index = 1, #frame.Activities do
            local activity = frame.Activities[index]
            if activity and activity:IsShown() then current.visible[#current.visible + 1] = activity end
        end
    end
    if #current.visible == 0 then
        self.pendingOpeningRevealPrivacy = false
        if frame.Activities then for index = 1, #frame.Activities do if frame.Activities[index] then frame.Activities[index]:SetAlpha(1) end end end
        self:StartFrameIdleAnimation()
        ReleaseSessionReferences(false)
        return false
    end

    SortActivitiesVisually(current.visible)
    current.rewardSummary = NS.RewardRarity and NS.RewardRarity:AnalyzeActivities(current.visible) or nil
    self.rewardRaritySummary = current.rewardSummary
    current.revealMode = VaultSkinsDB and VaultSkinsDB.revealMode or "sequential"
    local useCovers = current.revealMode == "click" or current.revealMode == "jackpot"
    self.revealActive = true
    self.pendingOpeningRevealPrivacy = false
    self.adaptiveRevealAudioActive = true
    self.revealedActivities = current.revealed
    self.lastRewardSoundAt = nil
    self.lastRewardSoundPriority = nil
    SetRevealGC(true)

    for index = 1, #current.visible do
        local activity = current.visible[index]
        local activitySkin = self:CreateActivitySkin(activity)
        if activitySkin then
            if activitySkin.CardRevealGroup and activitySkin.CardRevealGroup:IsPlaying() then activitySkin.CardRevealGroup:Stop() end
            StopSpecialRewardAnimations(activitySkin)
            activity:SetAlpha(0)
            if useCovers then
                if current.revealMode == "jackpot" then ConfigureJackpotRevealCover(activity, current.theme)
                else ConfigureClickRevealCover(activity, current.theme) end
            else
                HideRevealCover(activity)
            end
        else
            current.revealed[activity] = true
            activity:SetAlpha(1)
        end
        if activity.hasRewards then current.rewardCount = current.rewardCount + 1 end
    end

    local jackpotDelay
    if current.revealMode == "jackpot" then jackpotDelay = PlayJackpotShuffle(current) end
    BuildRevealSchedule(preset, current.revealMode, current.jackpotTarget, jackpotDelay)
    local safetyDelay = REVEAL_SAFETY_DELAYS[current.revealMode] or REVEAL_SAFETY_DELAYS.default
    ScheduleCall(REVEAL_GROUP, REVEAL_SAFETY_TIMER, safetyDelay, SafetyEvent, current)

    local frameSoundWillSync = VaultSkinsDB and VaultSkinsDB.soundSync and VaultSkinsDB.frameSounds ~= false and VaultSkinsDB.frameAnimationMode ~= "off" and FrameControlEnabled("frameOpeningAnimation")
    if not frameSoundWillSync then PlayVaultSound("frameOpen", current.theme) end
    local mainSkin = self:CreateMainSkin(frame)
    local outerGlow = current.theme.outerGlow or current.theme.frameAccent or current.theme.accent
    mainSkin.OpeningGlow:SetBackdropBorderColor(outerGlow.r, outerGlow.g, outerGlow.b, 1)
    CreatePulseAnimation(mainSkin.OpeningGlow, current.animationStyle.openingGlowAlpha or 0.58, 0.12, 0.04, 0.34, 0, true):Play()

    current.latestDelay = 0
    if current.revealMode == "click" then
        current.remaining = #current.visible
        for index = 1, #current.visible do
            local activity = current.visible[index]
            local cover = activity.VaultSkins and activity.VaultSkins.RevealCover
            if not cover and RevealActivity(current, activity, activity.hasRewards and "reward" or "slot") then current.remaining = current.remaining - 1 end
        end
        if current.remaining <= 0 then ScheduleCall(REVEAL_GROUP, nil, 0.24, FinishEvent, current) end
    else
        for index = 1, current.scheduleCount do
            local activity = current.scheduleActivities[index]
            local delay = current.scheduleDelays[index]
            local sound = current.scheduleSounds[index]
            current.latestDelay = math.max(current.latestDelay, delay)
            ScheduleCall(REVEAL_GROUP, nil, delay, RevealEvent, current, activity, sound, current.revealMode == "jackpot" and activity and activity.hasRewards)
        end
    end

    ScheduleCall(REVEAL_GROUP, nil, 0, FrameEffectsEvent, current)
    if current.revealMode ~= "click" then
        local total = current.latestDelay + math.max(preset.fadeDuration * (current.revealStyle.fadeScale or 1), preset.borderFall) + (current.revealStyle.extraDuration or 0) + 0.08
        ScheduleCall(REVEAL_GROUP, nil, total, FinishEvent, current)
        ScheduleCall(REVEAL_GROUP, REVEAL_WATCHDOG_TIMER, total + 1.0, WatchdogEvent, current)
    end
    return true
end

-- ============================ SOUND PREVIEW API =============================
local function PreviewSoundEvent(previewToken, entry, theme)
    if previewToken ~= Skin.soundPreviewToken then return end
    if entry.previewJackpot and PlaySoundFile then pcall(PlaySoundFile, CUSTOM_JACKPOT_SOUND_FILES.jackpotReward, "SFX")
    else PlayVaultSound(entry.kind, theme, entry.force) end
end

local PREVIEW_STANDARD = {
    { kind = "frameOpen", delay = 0.00 },
    { kind = "revealSoft", delay = 0.46 },
    { kind = "revealSoft", delay = 0.98 },
    { kind = "final", delay = 1.62 },
}
local PREVIEW_JACKPOT = {
    { kind = "jackpotTick", delay = 0.00 }, { kind = "jackpotTickAlt", delay = 0.14 },
    { kind = "jackpotTick", delay = 0.31 }, { kind = "jackpotTickAlt", delay = 0.52 },
    { kind = "jackpotStop", delay = 0.82 }, { kind = "reward", delay = 1.12, force = true, previewJackpot = true },
    { kind = "jackpotWin", delay = 1.72 },
}

function Skin:PreviewSoundSequence(mode)
    if not (VaultSkinsDB and VaultSkinsDB.sounds) then return false end
    CancelGroup(SOUND_PREVIEW_GROUP, "new sound preview")
    self.soundPreviewToken = (self.soundPreviewToken or 0) + 1
    self.lastRewardSoundAt = nil
    local token = self.soundPreviewToken
    local theme = self:GetTheme()
    local sequence = mode == "jackpot" and PREVIEW_JACKPOT or PREVIEW_STANDARD
    if mode ~= "jackpot" then
        local preset = VaultSkinsDB.animationPreset or "standard"
        local kind = ({ off = "revealQuick", fast = "revealQuick", standard = "revealSoft", pulse = "revealPulse", spotlight = "revealSpotlight", cinematic = "revealCinematic" })[preset] or "revealSoft"
        PREVIEW_STANDARD[2].kind = kind
        PREVIEW_STANDARD[3].kind = kind
    end
    for index = 1, #sequence do
        local entry = sequence[index]
        ScheduleCall(SOUND_PREVIEW_GROUP, nil, entry.delay, PreviewSoundEvent, token, entry, theme)
    end
    return true
end
