-- core.lua

local addonName = ...
local LSM = LibStub("LibSharedMedia-3.0")

CCS = CCS or {}

-- Set true to enable the Mythic+ module
CCS.MPLUS_ENABLED = true

-- OnReady callback queue. ui.lua (and any future file) registers init callbacks
-- here at file-scope load time; core.lua fires them after AceDB is initialized,
-- in registration order. Callers should not rely on ADDON_LOADED for init.
CCS._onReadyQueue = CCS._onReadyQueue or {}
CCS._ready = false
function CCS.OnReady(fn)
    if CCS._ready then
        fn()
    else
        CCS._onReadyQueue[#CCS._onReadyQueue + 1] = fn
    end
end

--------------------------------------------------
-- LSM Registration
--------------------------------------------------

local basePath = "Interface\\AddOns\\" .. addonName .. "\\sounds\\"

local lsmSounds = {
    "break","breath","burn","dot","marked","move","soak","spread","targeted","drop","fixate","pull","stack","absorb","debuff","charge","clear","knock","spikes"
}

for _, name in ipairs(lsmSounds) do
    LSM:Register("sound", "CCS: " .. name, basePath .. name .. ".ogg")
end


local soundPaths = {}
for _, name in ipairs(lsmSounds) do
    soundPaths[name] = basePath .. name .. ".ogg"
end

local function resolvePath(s)
    if not s then return nil end
    if type(s) ~= "string" then return nil end
    if s:sub(1, 5) == "file:" then
        return basePath .. s:sub(6) .. ".ogg"
    end
    if soundPaths[s] then return soundPaths[s] end
    return LSM:Fetch("sound", s)
end
CCS.ResolvePath = resolvePath

--------------------------------------------------
-- Difficulty
--------------------------------------------------

local function getCurrentDifficulty()
    local _, instanceType, difficultyID = GetInstanceInfo()
    if instanceType == "raid" then
        if difficultyID == 16 then return "M"
        elseif difficultyID == 15 then return "H"
        end
    elseif instanceType == "party" then
        -- M+ difficulty IDs: 8 (M+), 9 (Challenge), 10 (scenario), 23 (Timewalking M+)
        if difficultyID == 8 or difficultyID == 9 or difficultyID == 23 then
            return "M"
        end
    end
    return nil
end

local function getInstanceType()
    local _, instanceType = GetInstanceInfo()
    return instanceType  -- "raid", "party", "none", etc.
end

CCS.getCurrentDifficulty = getCurrentDifficulty

--------------------------------------------------
-- AceDB
--------------------------------------------------

local dbDefaults = {
    profile = {},
    char = {
        minimap              = { minimapPos = 225, hide = false },
        module               = "raid",
        activeDungeon        = "__all__",
        activeRaid           = "__all__",
        customTimerOverride  = false,
        showAllBosses        = {},
    },
}

-- Ordered list of active M+ dungeons
CCS.MplusDungeons = {
    { key = "magisters_terrace",       label = "Magister's Terrace",      color = "|cffda8b45", data = function() return CCS_Spells_Mplus_MagistersTerrace      end },
    { key = "maisara_caverns",         label = "Maisara Caverns",         color = "|cff7ec87e", data = function() return CCS_Spells_Mplus_MaisaraCaverns         end },
    { key = "nexus_point_xenas",       label = "Nexus-Point Xenas",       color = "|cff6aacdc", data = function() return CCS_Spells_Mplus_NexusPointXenas        end },
    { key = "windrunner_spire",        label = "Windrunner Spire",        color = "|cffe8c46a", data = function() return CCS_Spells_Mplus_WindrunnerSpire        end },
    { key = "algethar_academy",        label = "Algeth'ar Academy",       color = "|cffc17de8", data = function() return CCS_Spells_Mplus_AlgetharAcademy        end },
    { key = "pit_of_saron",            label = "Pit of Saron",            color = "|cff9dbde8", data = function() return CCS_Spells_Mplus_PitOfSaron             end },
    { key = "seat_of_the_triumvirate", label = "Seat of the Triumvirate", color = "|cffdc8fe0", data = function() return CCS_Spells_Mplus_SeatOfTheTriumvirate   end },
    { key = "skyreach",                label = "Skyreach",                color = "|cffe8e06a", data = function() return CCS_Spells_Mplus_Skyreach               end },
}

local db

function CCS.GetProfile()
    if not db then return { warnEnabled={}, warnOverride={}, countdownEnabled={}, countdownOverride={} } end
    local p = db.profile
    if rawget(p, "warnEnabled")       == nil then rawset(p, "warnEnabled",       {}) end
    if rawget(p, "warnOverride")      == nil then rawset(p, "warnOverride",       {}) end
    if rawget(p, "countdownEnabled")  == nil then rawset(p, "countdownEnabled",   {}) end
    if rawget(p, "countdownOverride") == nil then rawset(p, "countdownOverride",  {}) end
    return p
end

function CCS.GetChar()
    if not db then return dbDefaults.char end
    return db.char
end

function CCS.GetProfileName()
    if not db then return "Default" end
    return db:GetCurrentProfile()
end

function CCS.GetProfileNames()
    if not db then return {"Default"} end
    local names = {}
    db:GetProfiles(names)
    table.sort(names)
    return names
end

function CCS.SetActiveProfile(name)
    if not db then return end
    db:SetProfile(name)
end

function CCS.NewProfile(name)
    if not db then return end
    db:SetProfile(name)
end

function CCS.CopyProfile(sourceName)
    if not db then return end
    db:CopyProfile(sourceName)
end

function CCS.DeleteProfile(name)
    if not db then return end
    db:DeleteProfile(name)
end

function CCS.ResetProfile()
    if not db then return end
    db:ResetProfile()
end

function CCS.GetDB()
    return db
end

local function applyModule()
    local m = (db and db.char.module) or "raid"
    if not CCS.MPLUS_ENABLED and m == "mplus" then
        if db then db.char.module = "raid" end
        m = "raid"
    end
    if m == "mplus" and CCS.MPLUS_ENABLED then
        local key = (db and db.char.activeDungeon) or "__all__"
        if key == "__all__" then
            local combined = {}
            for _, dungeon in ipairs(CCS.MplusDungeons) do
                local data = dungeon.data()
                if data then
                    for _, entry in ipairs(data) do
                        combined[#combined + 1] = entry
                    end
                end
            end
            CCS_Spells = combined
            return
        end
        for _, dungeon in ipairs(CCS.MplusDungeons) do
            if dungeon.key == key then
                local data = dungeon.data()
                CCS_Spells = data or {}
                return
            end
        end
        -- Stored dungeon key no longer exists in the current list; reset to __all__
        if db then db.char.activeDungeon = "__all__" end
        local combined = {}
        for _, dungeon in ipairs(CCS.MplusDungeons) do
            local data = dungeon.data()
            if data then
                for _, entry in ipairs(data) do
                    combined[#combined + 1] = entry
                end
            end
        end
        CCS_Spells = combined
    else
        local raidKey = (db and db.char.activeRaid) or "__all__"
        if raidKey == "__all__" then
            CCS_Spells = CCS_Spells_Raid
        else
            local filtered = {}
            for _, entry in ipairs(CCS_Spells_Raid) do
                if entry.raid == raidKey then
                    filtered[#filtered + 1] = entry
                end
            end
            CCS_Spells = filtered
        end
    end
end

CCS.ApplyModule = applyModule

function CCS.SetDungeon(key)
    if not db or not CCS.MPLUS_ENABLED then return end
    if key ~= "__all__" then
        local valid = false
        for _, dungeon in ipairs(CCS.MplusDungeons) do
            if dungeon.key == key then valid = true; break end
        end
        if not valid then
            print("|cffff5555CCS:|r Unknown dungeon '" .. tostring(key) .. "'.")
            return
        end
    end
    db.char.activeDungeon = key
    applyModule()
    CCS.RefreshAll()
    if CCS._fullRebuild then CCS._fullRebuild() end
end

function CCS.GetActiveDungeon()
    return (db and db.char.activeDungeon) or "__all__"
end

-- Returns ordered list of unique raid names from CCS_Spells_Raid
function CCS.GetRaidList()
    local seen = {}
    local list = {}
    for _, entry in ipairs(CCS_Spells_Raid) do
        if entry.raid and not seen[entry.raid] then
            seen[entry.raid] = true
            list[#list + 1] = entry.raid
        end
    end
    return list
end

function CCS.GetActiveRaid()
    return (db and db.char.activeRaid) or "__all__"
end

function CCS.SetRaid(name)
    if not db then return end
    if name ~= "__all__" then
        local valid = false
        for _, entry in ipairs(CCS_Spells_Raid) do
            if entry.raid == name then valid = true; break end
        end
        if not valid then
            print("|cffff5555CCS:|r Unknown raid '" .. tostring(name) .. "'.")
            return
        end
    end
    db.char.activeRaid = name
    applyModule()
    CCS.RefreshAll()
    if CCS._fullRebuild then CCS._fullRebuild() end
end

function CCS.SetModule(name)
    if name ~= "raid" and name ~= "mplus" then
        print("|cffff5555CCS:|r Unknown module '" .. name .. "'. Use 'raid' or 'mplus'.")
        return
    end
    if name == "mplus" and not CCS.MPLUS_ENABLED then
        print("|cffff5555CCS:|r Mythic+ module is not enabled.")
        return
    end
    if not db then return end
    db.char.module = name
    applyModule()
    CCS.RefreshAll()
    if CCS._fullRebuild then CCS._fullRebuild() end
end

function CCS.GetModule()
    if not CCS.MPLUS_ENABLED then return "raid" end
    return (db and db.char.module) or "raid"
end

function CCS.GetCustomTimerOverride()
    return CCS.GetChar().customTimerOverride == true
end

function CCS.SetCustomTimerOverride(val)
    CCS.GetChar().customTimerOverride = val
end

function CCS.GetShowAllBoss(bossKey)
    if not bossKey then return false end
    return CCS.GetChar().showAllBosses[bossKey] == true
end

function CCS.SetShowAllBoss(bossKey, val)
    if not bossKey then return end
    CCS.GetChar().showAllBosses[bossKey] = val or nil
    CCS.RefreshSounds()
end

-- Returns true if this ability is currently allowed to play sounds.
-- An ability is gated only if it's `advanced` and its boss's "Show all" is unchecked.
function CCS.IsAbilityActive(abilityKey)
    if not abilityKey then return true end
    -- Search raid data
    if CCS_Spells_Raid then
        for _, entry in ipairs(CCS_Spells_Raid) do
            if entry.abilities then
                for _, ab in ipairs(entry.abilities) do
                    if ab.key == abilityKey then
                        if ab.advanced and not CCS.GetShowAllBoss(entry.bossKey) then
                            return false
                        end
                        return true
                    end
                end
            end
        end
    end
    -- Search M+ data
    if CCS.MplusDungeons then
        for _, dungeon in ipairs(CCS.MplusDungeons) do
            local data = dungeon.data()
            if data then
                for _, entry in ipairs(data) do
                    if entry.abilities then
                        for _, ab in ipairs(entry.abilities) do
                            if ab.key == abilityKey then
                                if ab.advanced and not CCS.GetShowAllBoss(entry.bossKey) then
                                    return false
                                end
                                return true
                            end
                        end
                    end
                end
            end
        end
    end
    return true
end

--------------------------------------------------
-- Profile data accessors
--------------------------------------------------

function CCS.isWarnEnabled(key)
    return CCS.GetProfile().warnEnabled[key] == true
end

function CCS.SetWarnEnabled(key, val)
    CCS.GetProfile().warnEnabled[key] = val
end

function CCS.GetWarnOverride(key)
    return CCS.GetProfile().warnOverride[key]
end

function CCS.SetWarnOverride(key, soundKey)
    CCS.GetProfile().warnOverride[key] = soundKey
end

function CCS.IsCDEnabled(key, diff)
    local t = CCS.GetProfile().countdownEnabled[key]
    return t and t[diff] == true or false
end

function CCS.SetCDEnabled(key, diff, val)
    local p = CCS.GetProfile()
    p.countdownEnabled[key] = p.countdownEnabled[key] or {}
    p.countdownEnabled[key][diff] = val
end

function CCS.GetCountdownOverride(key, diff)
    local t = CCS.GetProfile().countdownOverride[key]
    return t and t[diff] or nil
end

function CCS.SetCountdownOverride(key, diff, soundKey)
    local p = CCS.GetProfile()
    p.countdownOverride[key] = p.countdownOverride[key] or {}
    p.countdownOverride[key][diff] = soundKey
end

-- Iterates abilities for a specific module ("raid" or "mplus")
local function iterateModuleSpells(module, fn)
    if module == "raid" then
        for _, entry in ipairs(CCS_Spells_Raid) do
            if entry.abilities then
                for _, ability in ipairs(entry.abilities) do fn(ability, false) end
            end
        end
    elseif module == "mplus" then
        for _, dungeon in ipairs(CCS.MplusDungeons) do
            local data = dungeon.data()
            if data then
                for _, entry in ipairs(data) do
                    if entry.abilities then
                        for _, ability in ipairs(entry.abilities) do fn(ability, true) end
                    end
                end
            end
        end
    end
end

function CCS.SetAllWarn(val, module)
    module = module or CCS.GetModule()
    local p = CCS.GetProfile()
    iterateModuleSpells(module, function(ability)
        -- A warning exists if:
        --   soundH/soundM is a string (the warning IS that string), or
        --   soundH/soundM is a table with a non-nil [1] (the warning slot)
        local function fieldHasWarn(f)
            if f == nil then return false end
            if type(f) == "table" then return f[1] ~= nil end
            return true  -- plain string = the warning
        end
        local hasDefault = fieldHasWarn(ability.soundH) or fieldHasWarn(ability.soundM)
        local hasOverride = p.warnOverride[ability.key] ~= nil
        -- Advanced abilities are only included by bulk-enable if the user has set an override.
        -- Bulk-disable still applies to them.
        if val and ability.advanced and not hasOverride then return end
        if not val or hasDefault or hasOverride then
            p.warnEnabled[ability.key] = val
        end
    end)
end

function CCS.SetAllCD(val, module)
    module = module or CCS.GetModule()
    local p = CCS.GetProfile()
    iterateModuleSpells(module, function(ability, isMplus)
        local function hasDefault(diff)
            local s = diff == "M" and ability.soundM or (diff ~= "M" and ability.soundH)
            return type(s) == "table" and s[2] ~= nil
        end
        local function hasOverride(diff)
            return p.countdownOverride[ability.key] and p.countdownOverride[ability.key][diff] ~= nil
        end
        local function isEnabled(diff)
            return p.countdownEnabled[ability.key] and p.countdownEnabled[ability.key][diff] == true
        end
        -- Advanced abilities are only included by bulk-enable if the user has set an override.
        -- Bulk-disable still applies to them.
        local anyOverride = hasOverride("H") or hasOverride("M")
        if val and ability.advanced and not anyOverride then return end
        if isMplus then
            if not val or hasDefault("M") or hasOverride("M") then
                p.countdownEnabled[ability.key] = p.countdownEnabled[ability.key] or {}
                p.countdownEnabled[ability.key].M = val
            elseif not val and isEnabled("M") then
                p.countdownEnabled[ability.key].M = false
            end
        else
            if ability.soundH ~= nil or hasOverride("H") or isEnabled("H") then
                if not val or hasDefault("H") or hasOverride("H") then
                    p.countdownEnabled[ability.key] = p.countdownEnabled[ability.key] or {}
                    p.countdownEnabled[ability.key].H = val
                end
            end
            if ability.soundM ~= nil or hasOverride("M") or isEnabled("M") then
                if not val or hasDefault("M") or hasOverride("M") then
                    p.countdownEnabled[ability.key] = p.countdownEnabled[ability.key] or {}
                    p.countdownEnabled[ability.key].M = val
                end
            end
        end
    end)
end

-- Returns "all_on", "all_off", or "mixed" based on warn-enabled flags for currently-visible abilities.
-- Skips abilities that cannot be toggled (no default warn and no user override).
-- Advanced abilities without a user override are excluded from "all_on" detection (since bulk-enable doesn't touch them),
-- but they are still counted toward "all_off" if their flag is off.
function CCS.GetBulkWarnState(module)
    module = module or CCS.GetModule()
    local p = CCS.GetProfile()
    local seen, onCount, offCount = 0, 0, 0
    iterateModuleSpells(module, function(ability)
        local function fieldHasWarn(f)
            if f == nil then return false end
            if type(f) == "table" then return f[1] ~= nil end
            return true
        end
        local hasDefault  = fieldHasWarn(ability.soundH) or fieldHasWarn(ability.soundM)
        local hasOverride = p.warnOverride[ability.key] ~= nil
        if hasDefault or hasOverride then
            -- Advanced without override: bulk-enable can't touch them, so don't include them in "all on" check.
            if ability.advanced and not hasOverride then return end
            seen = seen + 1
            if p.warnEnabled[ability.key] then onCount = onCount + 1 else offCount = offCount + 1 end
        end
    end)
    if seen == 0 then return "all_off" end
    if onCount == seen then return "all_on" end
    if offCount == seen then return "all_off" end
    return "mixed"
end

-- Returns "all_on", "all_off", or "mixed" based on countdown-enabled flags for currently-visible abilities.
-- Advanced abilities without a user override are excluded from "all_on" detection.
function CCS.GetBulkCDState(module)
    module = module or CCS.GetModule()
    local p = CCS.GetProfile()
    local seen, onCount, offCount = 0, 0, 0
    iterateModuleSpells(module, function(ability, isMplus)
        local function hasDefault(diff)
            local s = diff == "M" and ability.soundM or ability.soundH
            return type(s) == "table" and s[2] ~= nil
        end
        local function hasOverride(diff)
            return p.countdownOverride[ability.key] and p.countdownOverride[ability.key][diff] ~= nil
        end
        local function isEnabled(diff)
            return p.countdownEnabled[ability.key] and p.countdownEnabled[ability.key][diff] == true
        end
        local diffs = isMplus and {"M"} or {"H", "M"}
        for _, d in ipairs(diffs) do
            if hasDefault(d) or hasOverride(d) then
                -- Advanced without override: skip from "all on" calculation.
                if ability.advanced and not hasOverride(d) then
                    -- noop
                else
                    seen = seen + 1
                    if isEnabled(d) then onCount = onCount + 1 else offCount = offCount + 1 end
                end
            end
        end
    end)
    if seen == 0 then return "all_off" end
    if onCount == seen then return "all_on" end
    if offCount == seen then return "all_off" end
    return "mixed"
end




--------------------------------------------------
-- Sound resolution
--------------------------------------------------

-- Per-session dedupe for "could not resolve sound" warnings.
-- Keyed by ability.key + sound key + kind so each unique problem warns once.
local _warnedResolveFailures = {}
local function warnResolveFailure(kind, soundKey, abilityKey)
    local k = abilityKey .. "|" .. tostring(soundKey) .. "|" .. kind
    if _warnedResolveFailures[k] then return end
    _warnedResolveFailures[k] = true
    if kind == "warn" then
        print("|cffff9900CCS:|r Warning — could not resolve sound '" .. tostring(soundKey) .. "' for " .. abilityKey)
    else
        print("|cffff9900CCS:|r Warning — could not resolve countdown sound '" .. tostring(soundKey) .. "' for " .. abilityKey)
    end
end

local function resolveAbilitySounds(ability, diff)
    local paths = {}

    if CCS.isWarnEnabled(ability.key) then
        local warnField = ability.soundH or ability.soundM
        local defaultWarn
        if type(warnField) == "table" then
            defaultWarn = warnField[1]
        else
            defaultWarn = warnField
        end
        local warnKey = CCS.GetWarnOverride(ability.key) or defaultWarn
        if warnKey then
            local p = resolvePath(warnKey)
            if p then
                paths[#paths + 1] = p
            else
                warnResolveFailure("warn", warnKey, ability.key)
            end
        end
    end

    if CCS.IsCDEnabled(ability.key, diff) then
        local soundField = diff == "M" and ability.soundM or (diff ~= "M" and ability.soundH)
        local defaultCD = type(soundField) == "table" and soundField[2] or nil
        local ctOn = CCS.GetCustomTimerOverride()
        local cdKey = (ctOn and CCS.GetCountdownOverride(ability.key, diff)) or defaultCD
        -- If manual mode is off and there's no default, don't play anything
        if cdKey then
            local p = resolvePath(cdKey)
            if p then
                paths[#paths + 1] = p
            else
                warnResolveFailure("cd", cdKey, ability.key)
            end
        end
    end

    return #paths > 0 and paths or nil
end

--------------------------------------------------
-- Private Aura Registration
--------------------------------------------------

local handles = {}

local function unregisterAbility(key)
    if not handles[key] then return end
    for _, id in ipairs(handles[key]) do
        C_UnitAuras.RemovePrivateAuraAppliedSound(id)
    end
    handles[key] = nil
end

-- Returns a flat list of spellIDs for the given ability and difficulty.
-- Handles both scalar privateID and table { H = id, M = id }.
local function getPrivateIDs(ability, diff)
    local pid = ability.privateID
    if not pid then return {} end
    if type(pid) == "table" then
        if pid.H ~= nil or pid.M ~= nil then
            -- H/M split — pick the right difficulty
            local val = pid[diff]
            if not val then return {} end
            if type(val) == "table" then return val end  -- array of IDs for this diff
            return {val}
        end
        -- Plain array of IDs, same for both difficulties
        return pid
    end
    return {pid}
end

local function registerAbility(ability, diff, bossKey)
    if not ability then return end
    -- Skip advanced abilities if show all is not enabled for this boss
    if ability.advanced and not CCS.GetShowAllBoss(bossKey) then return end
    local ids = getPrivateIDs(ability, diff)
    if #ids == 0 then return end

    for _, spellID in ipairs(ids) do
        if not C_UnitAuras.AuraIsPrivate(spellID) then
            if diff then
                print("|cffff9900CCS:|r " .. ability.key .. " (spellID " .. spellID .. ") is not a private aura — skipping.")
            end
            return
        end
    end

    local paths = resolveAbilitySounds(ability, diff)
    if not paths then return end
    -- Defensive: ensure no stale handles exist for this key. Prevents duplicate
    -- sound playback if registerAbility is reached twice without an intervening
    -- unregister (e.g. racing zone-change / pending-refresh paths).
    if handles[ability.key] then unregisterAbility(ability.key) end
    handles[ability.key] = {}

    for _, spellID in ipairs(ids) do
        for _, path in ipairs(paths) do
            local id = C_UnitAuras.AddPrivateAuraAppliedSound({
                unitToken     = "player",
                spellID       = spellID,
                soundFileName = path,
                outputChannel = "Master",
            })
            if id then
                handles[ability.key][#handles[ability.key] + 1] = id
            end
        end
    end
end

local function unregisterAll()
    for key in pairs(handles) do
        unregisterAbility(key)
    end
end

local function registerAll()
    local diff = getCurrentDifficulty()
    if not diff then return end
    local instType = getInstanceType()

    if instType == "raid" then
        for _, entry in ipairs(CCS_Spells_Raid) do
            if entry.abilities then
                for _, ability in ipairs(entry.abilities) do
                    registerAbility(ability, diff, entry.bossKey)
                end
            end
        end
    elseif instType == "party" and CCS.MPLUS_ENABLED then
        for _, dungeon in ipairs(CCS.MplusDungeons) do
            local data = dungeon.data()
            if data then
                for _, entry in ipairs(data) do
                    if entry.abilities then
                        for _, ability in ipairs(entry.abilities) do
                            registerAbility(ability, diff, entry.bossKey)
                        end
                    end
                end
            end
        end
    end
end

--------------------------------------------------
-- Refresh queue
--------------------------------------------------

local pendingRefreshAll = false
local pendingRefreshKeys = {}

local function flushPending()
    if pendingRefreshAll then
        pendingRefreshAll = false
        pendingRefreshKeys = {}
        unregisterAll()
        registerAll()
    else
        for key, entry in pairs(pendingRefreshKeys) do
            unregisterAbility(key)
            local diff = getCurrentDifficulty()
            if diff then registerAbility(entry.ability, diff, entry.bossKey) end
        end
        pendingRefreshKeys = {}
    end
end

--------------------------------------------------
-- Public API
--------------------------------------------------

function CCS.RefreshAbility(key, ability, bossKey)
    if InCombatLockdown() then
        pendingRefreshKeys[key] = {ability = ability, bossKey = bossKey}
        return
    end
    unregisterAbility(key)
    local diff = getCurrentDifficulty()
    if diff then registerAbility(ability, diff, bossKey) end
end

function CCS.RefreshSounds()
    if InCombatLockdown() then
        pendingRefreshAll = true
        return
    end
    unregisterAll()
    registerAll()
end

function CCS.RefreshAll()
    if InCombatLockdown() then
        pendingRefreshAll = true
        return
    end
    unregisterAll()
    registerAll()
end

function CCS.EnableAll()
    CCS.SetAllWarn(true)
    CCS.SetAllCD(true)
    CCS.RefreshAll()
end

function CCS.DisableAll()
    CCS.SetAllWarn(false)
    CCS.SetAllCD(false)
    CCS.RefreshAll()
end


--------------------------------------------------
-- Debug
--------------------------------------------------

function CCS.DebugProfile()
    local p = CCS.GetProfile()
    local diff = getCurrentDifficulty()
    print("|cffffff00CCS Debug — Profile:|r " .. CCS.GetProfileName() .. "  |cffaaaaaa(diff: " .. (diff or "none") .. ")|r")

    local warned, counted = 0, 0
    for _, entry in ipairs(CCS_Spells) do
        if entry.abilities then
            for _, ability in ipairs(entry.abilities) do
                local warnOn  = CCS.isWarnEnabled(ability.key)
                local warnOvr = CCS.GetWarnOverride(ability.key)
                local hOn     = CCS.IsCDEnabled(ability.key, "H")
                local mOn     = CCS.IsCDEnabled(ability.key, "M")
                local hOvr    = CCS.GetCountdownOverride(ability.key, "H")
                local mOvr    = CCS.GetCountdownOverride(ability.key, "M")
                if warnOn or warnOvr or hOn or mOn or hOvr or mOvr then
                    local parts = {}
                    if warnOn  then parts[#parts+1] = "|cff00ff00warn|r" end
                    if warnOvr then parts[#parts+1] = "|cffaaaaaarwarn=" .. warnOvr .. "|r" end
                    if hOn     then parts[#parts+1] = "|cff00ff00HC|r" end
                    if hOvr    then parts[#parts+1] = "|cffaaaaaaHC=" .. hOvr .. "|r" end
                    if mOn     then parts[#parts+1] = "|cff00ff00M|r" end
                    if mOvr    then parts[#parts+1] = "|cffaaaaaaM=" .. mOvr .. "|r" end
                    print("  " .. ability.key .. ": " .. table.concat(parts, "  "))
                    if warnOn then warned = warned + 1 end
                    if hOn or mOn then counted = counted + 1 end
                end
            end
        end
    end
    print("|cffaaaaaa  " .. warned .. " warn(s), " .. counted .. " countdown(s) enabled.|r")
end

function CCS.DebugSounds()
    local count = 0
    print("|cffffff00CCS Debug — Registered Sounds:|r")
    for key, ids in pairs(handles) do
        print("  " .. key .. ": " .. #ids .. " handle(s) — " .. table.concat(ids, ", "))
        count = count + 1
    end
    if count == 0 then
        print("|cffaaaaaa  No sounds currently registered.|r")
    else
        print("|cffaaaaaa  " .. count .. " ability/abilities registered.|r")
    end
end

local eventFrame = CreateFrame("Frame")
eventFrame:RegisterEvent("ADDON_LOADED")
eventFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
eventFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
eventFrame:RegisterEvent("ENCOUNTER_END")
eventFrame:RegisterEvent("ZONE_CHANGED_NEW_AREA")

local dbReady = false
local pendingEnterWorld = false

eventFrame:SetScript("OnEvent", function(self, event, ...)
    if event == "ADDON_LOADED" then
        local name = ...
        if name ~= addonName then return end

        -- Initialize AceDB
        db = LibStub("AceDB-3.0"):New("CCS_DB", dbDefaults, true)
        applyModule()

        -- Migrate old minimapAngle/minimapHidden to new LibDBIcon schema
        if CCS_DB_CHAR then
            if CCS_DB_CHAR.minimapAngle  ~= nil then db.char.minimap.minimapPos = CCS_DB_CHAR.minimapAngle  end
            if CCS_DB_CHAR.minimapHidden ~= nil then db.char.minimap.hide       = CCS_DB_CHAR.minimapHidden end
        end
        -- Migrate from old char fields if they exist (users upgrading from previous version)
        if db.char.minimapAngle ~= nil then
            db.char.minimap.minimapPos = db.char.minimapAngle
            db.char.minimapAngle = nil
        end
        if db.char.minimapHidden ~= nil then
            db.char.minimap.hide = db.char.minimapHidden
            db.char.minimapHidden = nil
        end

        -- Refresh sounds on any profile change, and notify UI panels
        db.RegisterCallback(CCS, "OnProfileChanged", function()
            CCS.RefreshAll()
            if CCS._onProfileChange then CCS._onProfileChange() end
        end)
        db.RegisterCallback(CCS, "OnProfileCopied",  function()
            CCS.RefreshAll()
            if CCS._onProfileChange then CCS._onProfileChange() end
        end)
        db.RegisterCallback(CCS, "OnProfileReset",   function()
            CCS.RefreshAll()
            if CCS._onProfileChange then CCS._onProfileChange() end
        end)

        dbReady = true
        CCS._ready = true
        self:UnregisterEvent("ADDON_LOADED")

        -- Fire queued OnReady callbacks in registration order
        for _, fn in ipairs(CCS._onReadyQueue) do
            local ok, err = pcall(fn)
            if not ok then
                print("|cffff5555CCS:|r OnReady callback error: " .. tostring(err))
            end
        end
        CCS._onReadyQueue = {}

        if pendingEnterWorld then
            pendingEnterWorld = false
            unregisterAll()
            C_Timer.After(0, function()
                registerAll()
            end)
        end

    elseif event == "PLAYER_ENTERING_WORLD" or event == "ZONE_CHANGED_NEW_AREA" then
        if not dbReady then
            pendingEnterWorld = true
            return
        end
        unregisterAll()
        local instType = getInstanceType()
        if instType ~= "raid" and instType ~= "party" then
            -- Not in a relevant instance; nothing to register
            return
        end
        -- Delay one frame so difficultyID is populated before registering
        C_Timer.After(0, function()
            registerAll()
        end)

    elseif event == "PLAYER_REGEN_ENABLED" then
        if pendingRefreshAll or next(pendingRefreshKeys) then
            flushPending()
            print("|cffffff00CCS:|r Settings applied after combat.")
        end

    elseif event == "ENCOUNTER_END" then
        -- sounds persist between pulls, no action needed
    end
end)