local NS = rawget(_G, "MCDVoiceCooldownNS") or {}
_G.MCDVoiceCooldownNS = NS

NS.Core = NS.Core or {}
NS.Core.CastSuccess = NS.Core.CastSuccess or {}

local CastSuccess = NS.Core.CastSuccess
local CONST = NS.Constants or {}

local ALL_CLASSES_ID = CONST.ALL_CLASSES_ID or 0
local ALL_SPECS_ID = CONST.ALL_SPECS_ID or 0
local OBJECT_TYPE_ITEM = CONST.OBJECT_TYPE_ITEM or "item"
local OBJECT_TYPE_SPELL = CONST.OBJECT_TYPE_SPELL or "spell"
local MODE_SOUND = CONST.MODE_SOUND or "sound"

local castSuccessCfg = {}
local callbacks = {}

local function SafeCall(name, ...)
    local fn = callbacks[name]
    if type(fn) == "function" then
        return fn(...)
    end
    return nil
end

local function GetOrderedEntryIndices(map)
    local value = SafeCall("getOrderedEntryIndices", map)
    if type(value) == "table" then
        return value
    end

    local indices = {}
    if type(map) ~= "table" then
        return indices
    end
    for index, entry in pairs(map) do
        index = tonumber(index) or 0
        if index >= 1 and type(entry) == "table" and (tonumber(entry.spellId) or 0) > 0 then
            indices[#indices + 1] = index
        end
    end
    table.sort(indices)
    return indices
end

local function GetEntry(map, index)
    local entry = SafeCall("getEntry", map, index)
    if type(entry) == "table" then
        return entry
    end
    entry = map and map[index]
    if type(entry) ~= "table" then
        return nil
    end
    local spellId = tonumber(entry.spellId) or 0
    if spellId <= 0 then
        return nil
    end
    return entry
end

local function ResolveObjectType(objectID, objectType)
    local value = SafeCall("resolveObjectType", objectID, objectType)
    if type(value) == "string" and value ~= "" then
        return value
    end
    objectType = tostring(objectType or OBJECT_TYPE_SPELL):lower()
    if objectType ~= OBJECT_TYPE_ITEM then
        objectType = OBJECT_TYPE_SPELL
    end
    return objectType
end

local function ResolveEntrySoundPath(entry)
    local value = SafeCall("resolveEntrySoundPath", entry)
    if type(value) == "string" then
        return value
    end
    return tostring(type(entry) == "table" and entry.soundPath or "")
end

local function GetObjectTriggerSpellID(objectID, objectType, triggerSpellID)
    local value = SafeCall("getObjectTriggerSpellID", objectID, objectType, triggerSpellID)
    value = tonumber(value) or 0
    if value > 0 then
        return value
    end
    return tonumber(triggerSpellID) or tonumber(objectID) or 0
end

local function ResolveItemTriggerForEntry(entry, quiet)
    local value = SafeCall("resolveItemTriggerForEntry", entry, quiet)
    if value ~= nil then
        return value
    end
    return true
end

local function GetCurrentClassSpec()
    local classID, specID = SafeCall("getCurrentClassSpec")
    return tonumber(classID) or 0, tonumber(specID) or 0
end

local function GetStoredEntryMap(classID, specID)
    local map = SafeCall("getStoredEntryMap", classID, specID)
    return type(map) == "table" and map or {}
end

local function GetCastSuccessMap(classID, specID)
    local map = SafeCall("getCastSuccessMap", classID, specID)
    return type(map) == "table" and map or {}
end

function CastSuccess:Configure(opts)
    opts = type(opts) == "table" and opts or {}
    callbacks.getCurrentClassSpec = opts.getCurrentClassSpec
    callbacks.getOrderedEntryIndices = opts.getOrderedEntryIndices
    callbacks.getEntry = opts.getEntry
    callbacks.getStoredEntryMap = opts.getStoredEntryMap
    callbacks.getCastSuccessMap = opts.getCastSuccessMap
    callbacks.resolveObjectType = opts.resolveObjectType
    callbacks.resolveItemTriggerForEntry = opts.resolveItemTriggerForEntry
    callbacks.getObjectTriggerSpellID = opts.getObjectTriggerSpellID
    callbacks.resolveEntrySoundPath = opts.resolveEntrySoundPath
    callbacks.queueNotification = opts.queueNotification
    return true
end

function CastSuccess:GetConfigTable()
    return castSuccessCfg
end

function CastSuccess:GetConfig(triggerSpellID)
    triggerSpellID = tonumber(triggerSpellID) or 0
    if triggerSpellID <= 0 then
        return nil
    end
    return castSuccessCfg[triggerSpellID]
end

function CastSuccess:Clear()
    wipe(castSuccessCfg)
    return true
end

function CastSuccess:Rebuild()
    wipe(castSuccessCfg)

    local classID, specID = GetCurrentClassSpec()

    local function addCastEntry(entry, index, scopeClassID, scopeSpecID)
        if type(entry) ~= "table" then
            return
        end
        local objectID = tonumber(entry.spellId) or 0
        local objectType = ResolveObjectType(objectID, entry.objectType)
        if objectType == OBJECT_TYPE_ITEM then
            ResolveItemTriggerForEntry(entry, true)
        end
        local triggerSpellID = GetObjectTriggerSpellID(objectID, objectType, entry.triggerSpellID or entry.triggerSpellId)
        if objectID > 0 and triggerSpellID > 0 then
            -- 当前专精配置会覆盖全职业/全专精配置。
            castSuccessCfg[triggerSpellID] = {
                spellId = objectID,
                objectID = objectID,
                objectType = objectType,
                triggerSpellID = triggerSpellID,
                spellName = tostring(entry.spellName or ""),
                notifyMode = tostring(entry.notifyMode or MODE_SOUND),
                ttsText = tostring(entry.ttsText or ""),
                ttsRate = math.max(-10, math.min(10, tonumber(entry.ttsRate) or 0)),
                soundPath = ResolveEntrySoundPath(entry),
                soundSource = tostring(entry.soundSource or ""),
                sharedMediaSound = tostring(entry.sharedMediaSound or entry.sharedMediaName or ""),
                delayEnabled = entry.delayEnabled == true,
                delaySeconds = math.max(0, tonumber(entry.delaySeconds) or 0),
                index = index,
                scopeClassID = tonumber(scopeClassID) or 0,
                scopeSpecID = tonumber(scopeSpecID) or 0,
            }
        end
    end

    local function addLegacyCastMap(castMap, scopeClassID, scopeSpecID)
        for _, index in ipairs(GetOrderedEntryIndices(castMap)) do
            addCastEntry(GetEntry(castMap, index), index, scopeClassID, scopeSpecID)
        end
    end

    local function addUnifiedCastMap(entryMap, scopeClassID, scopeSpecID)
        for _, index in ipairs(GetOrderedEntryIndices(entryMap)) do
            local entry = GetEntry(entryMap, index)
            if entry and tostring(entry.entryType or "cooldown") == "cast" then
                addCastEntry(entry, index, scopeClassID, scopeSpecID)
            end
        end
    end

    -- 优先级：全职业 < 当前职业全专精 < 当前职业当前专精。
    addLegacyCastMap(GetCastSuccessMap(ALL_CLASSES_ID, ALL_SPECS_ID), ALL_CLASSES_ID, ALL_SPECS_ID)
    addUnifiedCastMap(GetStoredEntryMap(ALL_CLASSES_ID, ALL_SPECS_ID), ALL_CLASSES_ID, ALL_SPECS_ID)

    addLegacyCastMap(GetCastSuccessMap(classID, ALL_SPECS_ID), classID, ALL_SPECS_ID)
    addUnifiedCastMap(GetStoredEntryMap(classID, ALL_SPECS_ID), classID, ALL_SPECS_ID)

    if specID ~= ALL_SPECS_ID then
        addLegacyCastMap(GetCastSuccessMap(classID, specID), classID, specID)
        addUnifiedCastMap(GetStoredEntryMap(classID, specID), classID, specID)
    end

    return true
end

function CastSuccess:Queue(triggerSpellID, cfg)
    cfg = cfg or self:GetConfig(triggerSpellID)
    if type(cfg) ~= "table" then
        return false
    end

    local result = SafeCall("queueNotification", triggerSpellID, cfg)
    return result == true
end

function CastSuccess:HandleSpellcastSucceeded(spellId)
    spellId = tonumber(spellId) or 0
    if spellId <= 0 then
        return false
    end

    local cfg = self:GetConfig(spellId)
    if not cfg then
        return false
    end

    return self:Queue(spellId, cfg)
end
