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

local EntryStore = NS.EntryStore or {}
NS.EntryStore = EntryStore

local L = NS.L or function(key, ...)
    if select("#", ...) > 0 then
        return string.format(tostring(key), ...)
    end
    return tostring(key)
end

local CONST = NS.Constants or {}
local Utils = NS.Utils or {}
local CollectionStore = NS.CollectionStore or {}

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

local function GetOptions(owner)
    return owner or NS.AceOptions or {}
end

local function GetApi()
    return NS.API
end

local function TrimText(value)
    if Utils.TrimText then
        return Utils.TrimText(value)
    end
    return tostring(value or ""):match("^%s*(.-)%s*$")
end

local function GetFileNameFromPath(value)
    if Utils.GetFileNameFromPath then
        return Utils.GetFileNameFromPath(value)
    end
    local path = tostring(value or ""):gsub("/", "\\")
    return path:match("([^\\]+)$") or path
end

local function BuildEntryKey(classID, specID, index)
    if CollectionStore.BuildEntryKey then
        return CollectionStore.BuildEntryKey(classID, specID, index)
    end
    return string.format("%d:%d:%d", tonumber(classID) or 0, tonumber(specID) or 0, tonumber(index) or 0)
end

local function ParseEntryKey(key)
    if CollectionStore.ParseEntryKey then
        return CollectionStore.ParseEntryKey(key)
    end
    local classID, specID, index = tostring(key or ""):match("^(%-?%d+):(%-?%d+):(%-?%d+)$")
    return tonumber(classID) or 0, tonumber(specID) or 0, tonumber(index) or 0
end

local function ParseGroupKey(key)
    if CollectionStore.ParseGroupKey then
        return CollectionStore.ParseGroupKey(key)
    end
    local classID, specID, groupID = tostring(key or ""):match("^group:(%-?%d+):(%-?%d+):(.+)$")
    return tonumber(classID) or 0, tonumber(specID) or 0, tostring(groupID or "")
end

local function EnsureRootDB()
    if CollectionStore.EnsureRootDB then
        return CollectionStore.EnsureRootDB()
    end
    if type(MCDVoiceCooldownDB) ~= "table" then
        MCDVoiceCooldownDB = {}
    end
    return MCDVoiceCooldownDB
end

local function EnsureCollectionScope(classID, specID)
    if CollectionStore.EnsureCollectionScope then
        return CollectionStore.EnsureCollectionScope(classID, specID)
    end
    return nil
end

local function NormalizeCollectionScope(scope, entryMap, scopeClassID, scopeSpecID)
    if CollectionStore.NormalizeCollectionScope then
        return CollectionStore.NormalizeCollectionScope(scope, entryMap, scopeClassID, scopeSpecID, GetApi())
    end
    return scope
end

local function RemoveEntryKeyFromAllCollectionScopes(entryKey)
    if CollectionStore.RemoveEntryKeyFromAllCollectionScopes then
        return CollectionStore.RemoveEntryKeyFromAllCollectionScopes(entryKey)
    end
end

local function RequestNativeUIRefresh(reason)
    if NS.UI and NS.UI.MainFrame then
        if type(NS.UI.MainFrame.RequestRefresh) == "function" then
            NS.UI.MainFrame:RequestRefresh(reason or "list")
            return true
        elseif type(NS.UI.MainFrame.Refresh) == "function" then
            NS.UI.MainFrame:Refresh()
            return true
        end
    end
    return false
end

local function IsLoadedScope(api, classID, specID, currentClassID, currentSpecID)
    classID = tonumber(classID) or 0
    specID = tonumber(specID) or 0
    currentClassID = tonumber(currentClassID) or 0
    currentSpecID = tonumber(currentSpecID) or 0
    if api and type(api.IsActiveScopeLoaded) == "function" then
        return api.IsActiveScopeLoaded(classID, specID, currentClassID, currentSpecID)
    end
    if classID == ALL_CLASSES_ID then
        return true
    end
    return classID == currentClassID and (specID == currentSpecID or specID == ALL_SPECS_ID)
end

local function NormalizeEntryTypeValue(value)
    value = tostring(value or "cooldown")
    if value ~= "cast" then
        value = "cooldown"
    end
    return value
end

local function NormalizeObjectTypeValue(value)
    value = tostring(value or OBJECT_TYPE_SPELL):lower()
    if value ~= OBJECT_TYPE_ITEM then
        value = OBJECT_TYPE_SPELL
    end
    return value
end

local function SameEntryIdentity(left, right)
    if type(left) ~= "table" or type(right) ~= "table" then
        return false
    end
    local leftID = tonumber(left.spellId or left.itemID) or 0
    local rightID = tonumber(right.spellId or right.itemID) or 0
    if leftID <= 0 or rightID <= 0 or math.floor(leftID) ~= math.floor(rightID) then
        return false
    end
    if NormalizeEntryTypeValue(left.entryType) ~= NormalizeEntryTypeValue(right.entryType) then
        return false
    end
    if NormalizeObjectTypeValue(left.objectType) ~= NormalizeObjectTypeValue(right.objectType) then
        return false
    end
    return true
end

local function DeleteMatchingEntriesFromMap(map, entry, classID, specID)
    if type(map) ~= "table" or type(entry) ~= "table" then
        return 0
    end
    local api = GetApi()
    local removed = 0
    for index, candidate in pairs(map) do
        local entryIndex = tonumber(index) or 0
        if entryIndex > 0 and SameEntryIdentity(candidate, entry) then
            if api and type(api.MarkEntryDeleted) == "function" then
                api.MarkEntryDeleted(classID, specID, candidate, entryIndex)
            end
            map[index] = nil
            RemoveEntryKeyFromAllCollectionScopes(BuildEntryKey(classID, specID, entryIndex))
            removed = removed + 1
        end
    end
    return removed
end

local function DeleteMatchingLegacyCastEntries(classID, specID, entry)
    if type(entry) ~= "table" or NormalizeEntryTypeValue(entry.entryType) ~= "cast" then
        return 0
    end
    local db = EnsureRootDB()
    local classMap = db.castSuccessConfigs and db.castSuccessConfigs[classID]
    local legacyMap = type(classMap) == "table" and classMap[specID] or nil
    if type(legacyMap) ~= "table" then
        return 0
    end
    local legacyProbe = {}
    for k, v in pairs(entry) do
        legacyProbe[k] = v
    end
    legacyProbe.entryType = "cast"
    return DeleteMatchingEntriesFromMap(legacyMap, legacyProbe, classID, specID)
end

local function ClearDeletedEntryMarker(classID, specID, entry, index)
    local api = GetApi()
    if api and type(api.ClearEntryDeletedMarker) == "function" then
        api.ClearEntryDeletedMarker(classID, specID, entry, index)
    end
end

local function BuildSoundDetailForEntry(options, entry, modeTts, modeSound)
    if type(entry) ~= "table" then
        return ""
    end

    options = GetOptions(options)
    local fields = type(options.ResolveSoundSourceFields) == "function" and options:ResolveSoundSourceFields(entry, modeTts, modeSound) or entry
    if fields.soundSource == "tts" or fields.notifyMode == tostring(modeTts or "tts") then
        local text = TrimText(entry.ttsText or "")
        if text == "" then
            text = L("TTS_READY_DEFAULT")
        end
        return L("SAVED_VOICE_TTS", text)
    end

    if fields.soundSource == "sharedmedia" and TrimText(fields.sharedMediaSound or "") ~= "" then
        return L("SAVED_VOICE_SHAREDMEDIA", fields.sharedMediaSound)
    end

    local normalizedPath = type(options.NormalizeSoundPath) == "function" and options:NormalizeSoundPath(fields.soundPath or entry.soundPath or "") or tostring(fields.soundPath or entry.soundPath or "")
    if fields.soundSource == "builtin" or (normalizedPath ~= "" and type(options.IsBuiltinSoundPath) == "function" and options:IsBuiltinSoundPath(normalizedPath)) then
        local displayName = normalizedPath
        if type(options.GetBuiltinSoundDisplayName) == "function" then
            displayName = options:GetBuiltinSoundDisplayName(normalizedPath)
        end
        return L("SAVED_VOICE_BUILTIN", displayName)
    end

    local customDisplayPath = TrimText(fields.customSoundPath or "")
    if customDisplayPath == "" then
        customDisplayPath = normalizedPath
    end
    if customDisplayPath == "" then
        customDisplayPath = tostring(entry.soundPath or "")
    end
    local fileName = GetFileNameFromPath(customDisplayPath)
    if TrimText(fileName) == "" then
        fileName = L("ENTRY_UNNAMED")
    end
    return L("SAVED_VOICE_CUSTOM", fileName)
end

function EntryStore:GetEntryMap(owner)
    local api = GetApi()
    local options = GetOptions(owner)
    local state = type(options.GetState) == "function" and options:GetState() or {}
    if not api then
        return {}
    end
    return api.GetStoredEntryMap(state.classID, state.specID)
end

function EntryStore:GetCurrentScopeEntryList(owner)
    local api = GetApi()
    local options = GetOptions(owner)
    local state = type(options.GetState) == "function" and options:GetState() or {}
    if not api then
        return {}
    end

    local entries = {}
    local map = self:GetEntryMap(options)
    local indices = type(api.GetOrderedEntryIndices) == "function" and api.GetOrderedEntryIndices(map) or {}
    local modeTts, modeSound = api.GetModes()

    for _, index in ipairs(indices) do
        local entry = api.GetEntry(map, index)
        if entry then
            local spellId = tonumber(entry.spellId) or 0
            local objectType = type(api.ResolveObjectType) == "function" and api.ResolveObjectType(spellId, entry.objectType) or tostring(entry.objectType or OBJECT_TYPE_SPELL)
            local spellName = TrimText(entry.spellName)
            if spellName == "" and spellId > 0 then
                spellName = type(api.ResolveObjectName) == "function" and api.ResolveObjectName(spellId, objectType) or api.ResolveSpellName(spellId)
            end

            entries[#entries + 1] = {
                key = BuildEntryKey(state.classID, state.specID, index),
                entryType = tostring(entry.entryType or "cooldown"),
                index = index,
                spellId = spellId,
                objectType = objectType,
                spellName = spellName ~= "" and spellName or tostring(spellId),
                modeText = tostring(entry.notifyMode or modeSound) == modeTts and "TTS" or L("MODE_SOUND"),
                soundDetail = BuildSoundDetailForEntry(options, entry, modeTts, modeSound),
                notifyMode = tostring(entry.notifyMode or modeSound),
                soundPath = tostring(entry.soundPath or ""),
                ttsText = tostring(entry.ttsText or ""),
                delayEnabled = entry.delayEnabled == true,
                delaySeconds = math.max(0, tonumber(entry.delaySeconds) or 0),
                checkTalent = entry.checkTalent == true and (tonumber(entry.talentId) or 0) > 0,
                talentId = tonumber(entry.talentId) or 0,
                talentName = TrimText(entry.talentName or ""),
                talentCD = tonumber(entry.talentCD) or 0,
                icon = type(api.ResolveObjectIcon) == "function" and api.ResolveObjectIcon(spellId, objectType) or (type(api.ResolveSpellIcon) == "function" and api.ResolveSpellIcon(spellId) or nil),
            }
        end
    end

    return entries
end

function EntryStore:GetAllSavedEntryList(owner)
    local api = GetApi()
    local options = GetOptions(owner)
    if not api then
        return {}
    end

    local entries = {}
    local db = MCDVoiceCooldownDB
    local root = type(db) == "table" and db.specConfigs or nil
    if type(root) ~= "table" then
        return entries
    end

    local currentClassID, currentSpecID = api.GetCurrentClassSpec()
    local modeTts, modeSound = api.GetModes()

    for classIDKey, classMap in pairs(root) do
        local classID = tonumber(classIDKey)
        if classID and classID >= 0 and type(classMap) == "table" then
            local className = api.ResolveClassName(classID)
            for specIDKey, specMap in pairs(classMap) do
                local specID = tonumber(specIDKey)
                if specID and specID >= 0 and type(specMap) == "table" then
                    local specName = api.ResolveSpecName(classID, specID)
                    local loadedTag = IsLoadedScope(api, classID, specID, currentClassID, currentSpecID) and L("LOADED_TAG") or ""
                    local indices = type(api.GetOrderedEntryIndices) == "function" and api.GetOrderedEntryIndices(specMap) or {}
                    for _, index in ipairs(indices) do
                        local entry = api.GetEntry(specMap, index)
                        if entry then
                            local spellId = tonumber(entry.spellId) or 0
                            local objectType = type(api.ResolveObjectType) == "function" and api.ResolveObjectType(spellId, entry.objectType) or tostring(entry.objectType or OBJECT_TYPE_SPELL)
                            local spellName = TrimText(entry.spellName)
                            if spellName == "" and spellId > 0 then
                                spellName = type(api.ResolveObjectName) == "function" and api.ResolveObjectName(spellId, objectType) or api.ResolveSpellName(spellId)
                            end

                            entries[#entries + 1] = {
                                key = BuildEntryKey(classID, specID, index),
                                entryType = tostring(entry.entryType or "cooldown"),
                                objectType = objectType,
                                classID = classID,
                                specID = specID,
                                index = index,
                                spellId = spellId,
                                spellName = spellName ~= "" and spellName or tostring(spellId),
                                modeText = tostring(entry.notifyMode or modeSound) == modeTts and "TTS" or L("MODE_SOUND"),
                                soundDetail = BuildSoundDetailForEntry(options, entry, modeTts, modeSound),
                                notifyMode = tostring(entry.notifyMode or modeSound),
                                soundPath = tostring(entry.soundPath or ""),
                                ttsText = tostring(entry.ttsText or ""),
                                delayEnabled = entry.delayEnabled == true,
                                delaySeconds = math.max(0, tonumber(entry.delaySeconds) or 0),
                                checkTalent = entry.checkTalent == true and (tonumber(entry.talentId) or 0) > 0,
                                talentId = tonumber(entry.talentId) or 0,
                                talentName = TrimText(entry.talentName or ""),
                                talentCD = tonumber(entry.talentCD) or 0,
                                icon = type(api.ResolveObjectIcon) == "function" and api.ResolveObjectIcon(spellId, objectType) or (type(api.ResolveSpellIcon) == "function" and api.ResolveSpellIcon(spellId) or nil),
                                scopeText = string.format("%s / %s%s", className, specName, loadedTag),
                                isLoaded = IsLoadedScope(api, classID, specID, currentClassID, currentSpecID),
                            }
                        end
                    end
                end
            end
        end
    end

    table.sort(entries, function(a, b)
        if a.classID ~= b.classID then
            return a.classID < b.classID
        end
        if a.specID ~= b.specID then
            return a.specID < b.specID
        end
        return a.index < b.index
    end)

    return entries
end

function EntryStore:LoadSelectedEntry(owner)
    local api = GetApi()
    local options = GetOptions(owner)
    local state = type(options.GetState) == "function" and options:GetState() or {}
    if not api then
        return
    end

    local classID, specID, selectedIndex = ParseEntryKey(state.selectedKey)
    if classID < 0 or specID < 0 or selectedIndex <= 0 then
        return
    end
    if classID == ALL_CLASSES_ID then
        specID = ALL_SPECS_ID
    end

    local map = api.GetStoredEntryMap(classID, specID)
    local entry = api.GetEntry(map, selectedIndex)
    if not entry then
        print("[MCD-CVA] " .. L("MSG_SELECTED_NOT_EXIST"))
        return
    end

    state.classID = classID
    state.specID = specID
    state.entryType = tostring(entry.entryType or "cooldown")
    state.spellId = tonumber(entry.spellId) or 0
    state.objectType = type(api.ResolveObjectType) == "function" and api.ResolveObjectType(state.spellId, entry.objectType) or tostring(entry.objectType or OBJECT_TYPE_SPELL)
    state.spellName = TrimText(entry.spellName)
    state.baseCD = tonumber(entry.baseCD) or 0
    state.fixedCD = true
    state.checkTalent = entry.checkTalent == true and (tonumber(entry.talentId) or 0) > 0
    state.talentId = tonumber(entry.talentId) or 0
    state.talentName = TrimText(entry.talentName or "")
    state.talentCD = tonumber(entry.talentCD) or 0
    state.delayEnabled = entry.delayEnabled == true
    state.delaySeconds = math.max(0, tonumber(entry.delaySeconds) or 0)
    local modeTts, modeSound = api.GetModes()
    state.ttsText = tostring(entry.ttsText or "")
    state.ttsRate = tonumber(entry.ttsRate) or 0
    local soundFields = type(options.ResolveSoundSourceFields) == "function" and options:ResolveSoundSourceFields(entry, modeTts, modeSound) or entry
    state.notifyMode = soundFields.notifyMode
    state.soundSource = soundFields.soundSource
    state.soundPath = soundFields.soundPath
    state.builtinSoundPath = soundFields.builtinSoundPath
    state.customSoundPath = soundFields.customSoundPath
    state.sharedMediaSound = soundFields.sharedMediaSound
    state.useCustomSound = soundFields.useCustomSound == true
    state.useSharedMediaSound = soundFields.useSharedMediaSound == true

    if state.spellName == "" and state.spellId > 0 then
        state.spellName = type(api.ResolveObjectName) == "function" and api.ResolveObjectName(state.spellId, state.objectType) or api.ResolveSpellName(state.spellId)
    end
end

function EntryStore:AutofillFromSpellId(owner)
    local api = GetApi()
    local options = GetOptions(owner)
    local state = type(options.GetState) == "function" and options:GetState() or {}
    if not api then
        return
    end

    local spellId = tonumber(state.spellId) or 0
    if spellId <= 0 then
        return
    end

    local objectType = type(api.ResolveObjectType) == "function" and api.ResolveObjectType(spellId, state.objectType) or OBJECT_TYPE_SPELL
    state.objectType = objectType

    local spellName = type(api.ResolveObjectName) == "function" and api.ResolveObjectName(spellId, objectType) or api.ResolveSpellName(spellId)
    if spellName ~= "" then
        state.spellName = spellName
    end

    local baseCD = type(api.ResolveObjectBaseCooldownSeconds) == "function" and api.ResolveObjectBaseCooldownSeconds(spellId, objectType) or api.ResolveSpellBaseCooldownSeconds(spellId)
    if baseCD and baseCD > 0 then
        state.baseCD = baseCD
    end
end

function EntryStore:AutofillFromTalentId(owner)
    local api = GetApi()
    local options = GetOptions(owner)
    local state = type(options.GetState) == "function" and options:GetState() or {}
    if not api then
        return
    end

    local talentId = tonumber(state.talentId) or 0
    if talentId <= 0 then
        state.talentName = ""
        return
    end

    local talentName = ""
    if type(api.ResolveTalentName) == "function" then
        talentName = api.ResolveTalentName(talentId)
    elseif type(api.ResolveSpellName) == "function" then
        talentName = api.ResolveSpellName(talentId)
    end
    if talentName ~= "" then
        state.talentName = talentName
    end
end

function EntryStore:SaveEntry(owner)
    local api = GetApi()
    local options = GetOptions(owner)
    local state = type(options.GetState) == "function" and options:GetState() or {}
    if not api then
        return
    end

    local classID = tonumber(state.classID) or 0
    local specID = tonumber(state.specID) or 0
    local selectedCollectionKeyForSave = tostring(state.selectedCollectionKey or "")
    if selectedCollectionKeyForSave == "" and tostring(state.selectedKey or ""):match("^group:") then
        selectedCollectionKeyForSave = tostring(state.selectedKey or "")
    end
    local saveGroupClassID, saveGroupSpecID, saveGroupID = ParseGroupKey(selectedCollectionKeyForSave)
    if saveGroupID ~= "" and saveGroupClassID >= 0 and saveGroupSpecID >= 0 then
        classID = saveGroupClassID
        specID = saveGroupSpecID
    end
    if classID == ALL_CLASSES_ID then
        specID = ALL_SPECS_ID
        state.specID = ALL_SPECS_ID
    end
    local spellId = tonumber(state.spellId) or 0
    local objectType = type(api.ResolveObjectType) == "function" and api.ResolveObjectType(spellId, state.objectType) or OBJECT_TYPE_SPELL
    local baseCD = tonumber(state.baseCD) or 0
    local checkTalent = state.checkTalent == true
    local talentId = tonumber(state.talentId) or 0
    local talentName = TrimText(state.talentName or "")
    local talentCD = tonumber(state.talentCD) or 0
    local notifyMode = tostring(state.notifyMode or select(2, api.GetModes()))
    local ttsText = tostring(state.ttsText or "")
    local delayEnabled = state.delayEnabled == true
    local delaySeconds = math.max(0, tonumber(state.delaySeconds) or 0)
    state.objectType = objectType
    local modeTts, modeSound = api.GetModes()
    local sourceForSave = {
        notifyMode = notifyMode,
        soundSource = state.soundSource,
        soundPath = state.soundPath,
        builtinSoundPath = state.builtinSoundPath,
        customSoundPath = state.customSoundPath,
        sharedMediaSound = state.sharedMediaSound,
        sharedMediaName = state.sharedMediaName,
    }
    if tostring(sourceForSave.soundSource or "") == "builtin" then
        sourceForSave.soundPath = state.builtinSoundPath or state.soundPath
    elseif tostring(sourceForSave.soundSource or "") == "custom" then
        sourceForSave.soundPath = state.customSoundPath or state.soundPath
    end
    local soundFields = type(options.ResolveSoundSourceFields) == "function" and options:ResolveSoundSourceFields(sourceForSave, modeTts, modeSound) or sourceForSave
    notifyMode = soundFields.notifyMode
    local soundPath = type(options.NormalizeSoundPath) == "function" and options:NormalizeSoundPath(soundFields.soundPath or "") or tostring(soundFields.soundPath or "")
    local soundSource = tostring(soundFields.soundSource or "builtin")
    local sharedMediaSound = TrimText(soundFields.sharedMediaSound or "")
    state.notifyMode = notifyMode
    state.soundSource = soundSource
    state.soundPath = soundPath
    state.builtinSoundPath = soundFields.builtinSoundPath
    state.customSoundPath = soundFields.customSoundPath
    state.sharedMediaSound = sharedMediaSound
    state.useCustomSound = soundFields.useCustomSound == true
    state.useSharedMediaSound = soundFields.useSharedMediaSound == true
    local entryType = tostring(state.entryType or "cooldown")
    if entryType ~= "cast" then
        entryType = "cooldown"
    end
    local isCooldownEntry = entryType == "cooldown"

    if classID < 0 or specID < 0 then
        print("[MCD-CVA] " .. L("MSG_INVALID_CLASS_SPEC"))
        return
    end
    if spellId <= 0 then
        print("[MCD-CVA] " .. L("MSG_INVALID_SPELL_ID"))
        return
    end
    if isCooldownEntry and baseCD <= 0 then
        print("[MCD-CVA] " .. L("MSG_INVALID_FIXED_CD"))
        return
    end
    if (not isCooldownEntry) then
        baseCD = 0
        checkTalent = false
        talentId = 0
        talentName = ""
        talentCD = 0
    else
        delayEnabled = false
        delaySeconds = 0
    end
    if (not isCooldownEntry) and delayEnabled and delaySeconds <= 0 then
        print("[MCD-CVA] " .. L("MSG_INVALID_DELAY_SECONDS"))
        return
    end
    if checkTalent and talentId <= 0 then
        print("[MCD-CVA] " .. L("MSG_NEED_TALENT_ID"))
        return
    end
    if checkTalent and talentCD <= 0 then
        print("[MCD-CVA] " .. L("MSG_NEED_TALENT_CD"))
        return
    end
    if checkTalent and talentName == "" and type(api.ResolveTalentName) == "function" then
        talentName = api.ResolveTalentName(talentId)
        state.talentName = talentName
    end
    if notifyMode == modeTts and TrimText(ttsText) == "" then
        print("[MCD-CVA] " .. L("MSG_NEED_TTS_TEXT"))
        return
    end
    if notifyMode == modeSound and TrimText(soundPath) == "" then
        print("[MCD-CVA] " .. L("MSG_NEED_SOUND_PATH"))
        return
    end

    local map = api.EnsureEntryMap(classID, specID)
    local selectedClassID, selectedSpecID, selectedIndex = ParseEntryKey(state.selectedKey)
    local targetIndex = 0

    if selectedClassID == classID and selectedSpecID == specID and selectedIndex >= 1 and api.GetEntry(map, selectedIndex) then
        targetIndex = selectedIndex
    else
        targetIndex = api.FindFirstFreeIndex(map) or 0
    end

    local indices = type(api.GetOrderedEntryIndices) == "function" and api.GetOrderedEntryIndices(map) or {}
    for _, index in ipairs(indices) do
        local entry = api.GetEntry(map, index)
        local existingID = tonumber(entry and entry.spellId) or 0
        local existingType = api and type(api.ResolveObjectType) == "function" and api.ResolveObjectType(existingID, entry and entry.objectType) or tostring(entry and entry.objectType or OBJECT_TYPE_SPELL)
        if entry
            and index ~= targetIndex
            and (tonumber(entry.spellId) or 0) == spellId
            and tostring(entry.entryType or "cooldown") == entryType
            and tostring(existingType) == tostring(objectType) then
            print("[MCD-CVA] " .. L("MSG_DUP_SPELL"))
            return
        end
    end

    if targetIndex <= 0 then
        print("[MCD-CVA] " .. L("MSG_SAVE_LIMIT"))
        return
    end

    local oldEntry = api.GetEntry(map, targetIndex)
    local savedEntry = {
        entryType = entryType,
        objectType = objectType,
        spellName = tostring(state.spellName or ""),
        spellId = math.floor(spellId),
        itemID = (objectType == OBJECT_TYPE_ITEM) and math.floor(spellId) or nil,
        baseCD = isCooldownEntry and tonumber(string.format("%.2f", baseCD)) or 0,
        fixedCD = true,
        checkTalent = isCooldownEntry and checkTalent and talentId > 0,
        talentId = (isCooldownEntry and checkTalent) and math.floor(talentId) or 0,
        talentName = (isCooldownEntry and checkTalent) and talentName or "",
        talentCD = (isCooldownEntry and checkTalent) and tonumber(string.format("%.2f", talentCD)) or 0,
        chargeInput = 1,
        notifyMode = notifyMode,
        ttsText = ttsText,
        ttsRate = math.max(-10, math.min(10, tonumber(state.ttsRate) or 0)),
        delayEnabled = (not isCooldownEntry) and delayEnabled == true,
        delaySeconds = (not isCooldownEntry) and tonumber(string.format("%.2f", delaySeconds)) or 0,
        soundPath = soundPath,
        soundSource = soundSource,
        sharedMediaSound = (soundSource == "sharedmedia" and sharedMediaSound ~= "") and sharedMediaSound or nil,
    }

    -- 已经保存过 triggerSpellID 的同一物品，不再重复解析；只沿用旧值。
    if objectType == OBJECT_TYPE_ITEM
        and oldEntry
        and tostring(oldEntry.objectType or "") == OBJECT_TYPE_ITEM
        and (tonumber(oldEntry.spellId) or 0) == spellId
        and (tonumber(oldEntry.triggerSpellID or oldEntry.triggerSpellId) or 0) > 0 then
        savedEntry.triggerSpellID = math.floor(tonumber(oldEntry.triggerSpellID or oldEntry.triggerSpellId) or 0)
        savedEntry.triggerSpellName = tostring(oldEntry.triggerSpellName or "")
    end

    if objectType == OBJECT_TYPE_ITEM and type(api.ResolveItemTriggerForEntry) == "function" then
        local resolved = api.ResolveItemTriggerForEntry(savedEntry, true)
        if not resolved then
            print("[MCD-CVA] 物品ID " .. tostring(spellId) .. " 已保存，但暂未解析到触发法术ID；脱战或物品资料加载后会自动补全。")
        end
    end

    ClearDeletedEntryMarker(classID, specID, savedEntry, targetIndex)
    map[targetIndex] = savedEntry

    local selectedCollectionKey = tostring(state.selectedCollectionKey or "")
    if selectedCollectionKey == "" and tostring(state.selectedKey or ""):match("^group:") then
        selectedCollectionKey = tostring(state.selectedKey or "")
    end
    if selectedCollectionKey ~= "" then
        local groupClassID, groupSpecID, groupID = ParseGroupKey(selectedCollectionKey)
        if groupID ~= "" and groupClassID == classID and groupSpecID == specID then
            local scope = EnsureCollectionScope(classID, specID)
            if scope and type(scope.groups[groupID]) == "table" then
                local entryKey = BuildEntryKey(classID, specID, targetIndex)
                RemoveEntryKeyFromAllCollectionScopes(entryKey)
                local group = scope.groups[groupID]
                if type(group.entries) ~= "table" then
                    group.entries = {}
                end
                group.entries[#group.entries + 1] = entryKey
                NormalizeCollectionScope(scope, map, classID, specID)
            end
        end
    end

    api.RebuildRuntimeConfig()
    if type(api.RebuildCastSuccessConfig) == "function" then
        api.RebuildCastSuccessConfig()
    end
    api.RefreshRuntimeCooldowns()
    state.selectedKey = BuildEntryKey(classID, specID, targetIndex)
    self:LoadSelectedEntry(options)
    if not RequestNativeUIRefresh("list") and type(api.RefreshPanel) == "function" then
        api.RefreshPanel()
    end
    print("[MCD-CVA] " .. L("MSG_CONFIG_SAVED"))
    return true
end

function EntryStore:DeleteSelectedEntry(owner, suppressRefresh)
    local api = GetApi()
    local options = GetOptions(owner)
    local state = type(options.GetState) == "function" and options:GetState() or {}
    if not api then
        return
    end

    local classID, specID, index = ParseEntryKey(state.selectedKey)
    if classID < 0 or specID < 0 or index <= 0 then
        print("[MCD-CVA] " .. L("MSG_CHOOSE_CONFIG"))
        return
    end

    local map = api.EnsureEntryMap(classID, specID)
    if not map then
        print("[MCD-CVA] " .. L("MSG_INVALID_CLASS_SPEC"))
        return
    end

    local deletedEntry = api.GetEntry(map, index)
    if deletedEntry and type(api.MarkEntryDeleted) == "function" then
        api.MarkEntryDeleted(classID, specID, deletedEntry, index)
    end

    map[index] = nil
    RemoveEntryKeyFromAllCollectionScopes(BuildEntryKey(classID, specID, index))
    DeleteMatchingLegacyCastEntries(classID, specID, deletedEntry)
    if type(options.ClearEditorFields) == "function" then
        options:ClearEditorFields()
    end
    api.RebuildRuntimeConfig()
    if type(api.RebuildCastSuccessConfig) == "function" then
        api.RebuildCastSuccessConfig()
    end
    api.RefreshRuntimeCooldowns()
    if not suppressRefresh then
        if not RequestNativeUIRefresh("list") and type(api.RefreshPanel) == "function" then
            api.RefreshPanel()
        end
    end
    print("[MCD-CVA] " .. L("MSG_CONFIG_DELETED"))
end
