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

NS.Core = NS.Core or {}
NS.Core.Runtime = NS.Core.Runtime or {}
NS.Runtime = NS.Core.Runtime

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

local OBJECT_TYPE_SPELL = CONST.OBJECT_TYPE_SPELL or "spell"
local MODE_SOUND = CONST.MODE_SOUND or "sound"
local UPDATE_INTERVAL_COMBAT = CONST.UPDATE_INTERVAL_COMBAT or 0.03
local UPDATE_INTERVAL_IDLE = CONST.UPDATE_INTERVAL_IDLE or 0.08

local frame = CreateFrame("Frame")
local cooldowns = {}
local delayedCastSuccess = {}
local updateElapsed = 0
local updating = false
local callbacks = {}

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

local function GetRuntimeUpdateInterval()
    local value = SafeCall("getUpdateInterval")
    value = tonumber(value) or 0
    if value > 0 then
        return value
    end
    if type(UnitAffectingCombat) == "function" and UnitAffectingCombat("player") then
        return UPDATE_INTERVAL_COMBAT
    end
    return UPDATE_INTERVAL_IDLE
end

local function NormalizeSoundPath(path)
    local value = SafeCall("normalizeSoundPath", path)
    if type(value) == "string" then
        return value
    end
    return tostring(path or ""):gsub("/", "\\")
end

local function ResolveObjectName(objectID, objectType)
    local value = SafeCall("resolveObjectName", objectID, objectType)
    if type(value) == "string" then
        return value
    end
    return ""
end

function Runtime:Configure(opts)
    opts = type(opts) == "table" and opts or {}
    callbacks.getUpdateInterval = opts.getUpdateInterval
    callbacks.playReady = opts.playReady
    callbacks.playCastSuccess = opts.playCastSuccess
    callbacks.normalizeSoundPath = opts.normalizeSoundPath
    callbacks.resolveObjectName = opts.resolveObjectName
    return true
end

function Runtime:GetCooldownTable()
    return cooldowns
end

function Runtime:GetDelayedCastSuccessTable()
    return delayedCastSuccess
end

function Runtime:WipeCooldowns()
    wipe(cooldowns)
    return true
end

function Runtime:ClearDelayedCastSuccessTimers()
    wipe(delayedCastSuccess)
    return true
end

function Runtime:SyncCooldownState(spellId, now)
    local cd = cooldowns[spellId]
    if not cd then
        return nil
    end

    now = tonumber(now) or GetTime()
    local maxCharge = math.max(1, math.floor(tonumber(cd.charge) or 1))
    local singleCD = tonumber(cd.singleCD) or 0
    if singleCD <= 0 then
        cooldowns[spellId] = nil
        return nil
    end

    if not cd.nextChargeAt then
        cd.nextChargeAt = now + singleCD
    end

    while cd.currentCharge < maxCharge and cd.nextChargeAt and now >= cd.nextChargeAt do
        cd.currentCharge = cd.currentCharge + 1
        if cd.currentCharge < maxCharge then
            cd.nextChargeAt = cd.nextChargeAt + singleCD
        else
            cd.nextChargeAt = nil
        end
    end

    return cd
end

function Runtime:EstimateCharges(cd)
    if not cd then
        return 0
    end
    local maxCharge = math.max(1, math.floor(tonumber(cd.charge) or 1))
    local currentCharge = math.floor(tonumber(cd.currentCharge) or 0)
    if currentCharge < 0 then
        currentCharge = 0
    end
    if currentCharge > maxCharge then
        currentCharge = maxCharge
    end
    return currentCharge
end

function Runtime:RefreshRuntimeCooldowns()
    local now = GetTime()
    for spellId in pairs(cooldowns) do
        self:SyncCooldownState(spellId, now)
    end
end

function Runtime:StartUpdate()
    if updating then
        return
    end

    updating = true
    updateElapsed = 0
    frame:SetScript("OnUpdate", function(_, elapsed)
        updateElapsed = updateElapsed + elapsed
        if updateElapsed < GetRuntimeUpdateInterval() then
            return
        end
        updateElapsed = 0

        local now = GetTime()
        local active = false

        for spellId, cd in pairs(cooldowns) do
            local wasUsable = self:EstimateCharges(cd) > 0
            cd = self:SyncCooldownState(spellId, now)
            if cd then
                local isUsable = self:EstimateCharges(cd) > 0
                if not wasUsable and isUsable and not cd.notified then
                    SafeCall("playReady", cd)
                    cd.notified = true
                end

                if self:EstimateCharges(cd) >= math.max(1, math.floor(tonumber(cd.charge) or 1)) then
                    cooldowns[spellId] = nil
                else
                    active = true
                end
            end
        end

        for triggerSpellID, timer in pairs(delayedCastSuccess) do
            local fireAt = tonumber(timer and timer.fireAt) or 0
            if fireAt > 0 and now >= fireAt then
                SafeCall("playCastSuccess", timer.cfg)
                delayedCastSuccess[triggerSpellID] = nil
            else
                active = true
            end
        end

        if not active then
            updating = false
            frame:SetScript("OnUpdate", nil)
        end
    end)
end

function Runtime:QueueCastSuccessNotification(triggerSpellID, cfg)
    if type(cfg) ~= "table" then
        return false
    end

    triggerSpellID = tonumber(triggerSpellID) or tonumber(cfg.triggerSpellID) or tonumber(cfg.spellId) or 0
    local delaySeconds = math.max(0, tonumber(cfg.delaySeconds) or 0)
    if not (cfg.delayEnabled == true and delaySeconds > 0 and triggerSpellID > 0) then
        return SafeCall("playCastSuccess", cfg) == true
    end

    -- 同一个成功施法触发器再次命中时，覆盖旧倒计时并从当前时刻重新计时。
    delayedCastSuccess[triggerSpellID] = {
        fireAt = GetTime() + delaySeconds,
        cfg = {
            spellId = tonumber(cfg.spellId) or 0,
            objectID = tonumber(cfg.objectID or cfg.spellId) or 0,
            objectType = tostring(cfg.objectType or OBJECT_TYPE_SPELL),
            triggerSpellID = triggerSpellID,
            spellName = tostring(cfg.spellName or ""),
            notifyMode = tostring(cfg.notifyMode or MODE_SOUND),
            ttsText = tostring(cfg.ttsText or ""),
            ttsRate = math.max(-10, math.min(10, tonumber(cfg.ttsRate) or 0)),
            soundPath = NormalizeSoundPath(cfg.soundPath or ""),
            soundSource = tostring(cfg.soundSource or ""),
            sharedMediaSound = tostring(cfg.sharedMediaSound or cfg.sharedMediaName or ""),
        },
    }
    self:StartUpdate()
    return true
end

function Runtime:StartCooldown(spellId, mappedSpellToPrimary, mappedRuntimeCfg)
    spellId = tonumber(spellId) or 0
    if spellId <= 0 or type(mappedSpellToPrimary) ~= "table" or type(mappedRuntimeCfg) ~= "table" or not mappedSpellToPrimary[spellId] then
        return
    end

    local primaryKey = mappedSpellToPrimary[spellId]
    local cfg = mappedRuntimeCfg[primaryKey]
    if not cfg or (tonumber(cfg.baseCD) or 0) <= 0 then
        return
    end

    -- 12.0.5：急速API在受污染插件逻辑中可能返回 Secret Number。
    -- 为避免 “attempt to compare/use a secret number value” 报错，冷却只按用户填写的固定CD计算。
    local singleCD = tonumber(cfg.baseCD) or 0
    singleCD = tonumber(string.format("%.2f", singleCD)) or singleCD
    if singleCD <= 0 then
        return
    end

    local maxCharge = math.max(1, math.floor(tonumber(cfg.chargeInput) or 1))
    local now = GetTime()
    local cd = self:SyncCooldownState(primaryKey, now)
    local displayName = cfg.spellName
    if tostring(displayName or "") == "" then
        displayName = ResolveObjectName(cfg.objectID or cfg.spellId, cfg.objectType)
    end

    if cd then
        cd.singleCD = singleCD
        cd.charge = maxCharge
        cd.spellName = displayName
        cd.objectType = cfg.objectType
        cd.objectID = cfg.objectID or cfg.spellId
        cd.notifyMode = tostring(cfg.notifyMode or MODE_SOUND)
        cd.ttsText = tostring(cfg.ttsText or "")
        cd.ttsRate = math.max(-10, math.min(10, tonumber(cfg.ttsRate) or 0))
        cd.soundPath = NormalizeSoundPath(cfg.soundPath or "")
        cd.soundSource = tostring(cfg.soundSource or "")
        cd.sharedMediaSound = tostring(cfg.sharedMediaSound or cfg.sharedMediaName or "")
        if cd.currentCharge > 0 then
            cd.currentCharge = cd.currentCharge - 1
        else
            cd.currentCharge = 0
            cd.nextChargeAt = now + singleCD
        end
        if cd.currentCharge <= 0 then
            cd.notified = false
            if not cd.nextChargeAt then
                cd.nextChargeAt = now + singleCD
            end
        end
    else
        local currentCharge = maxCharge - 1
        if currentCharge < 0 then
            currentCharge = 0
        end
        if currentCharge < maxCharge then
            cooldowns[primaryKey] = {
                singleCD = singleCD,
                charge = maxCharge,
                currentCharge = currentCharge,
                nextChargeAt = now + singleCD,
                spellName = displayName,
                objectType = cfg.objectType,
                objectID = cfg.objectID or cfg.spellId,
                notifyMode = tostring(cfg.notifyMode or MODE_SOUND),
                ttsText = tostring(cfg.ttsText or ""),
                ttsRate = math.max(-10, math.min(10, tonumber(cfg.ttsRate) or 0)),
                soundPath = NormalizeSoundPath(cfg.soundPath or ""),
                soundSource = tostring(cfg.soundSource or ""),
                sharedMediaSound = tostring(cfg.sharedMediaSound or cfg.sharedMediaName or ""),
                notified = (currentCharge > 0),
                spellId = cfg.objectID or cfg.spellId,
            }
        end
    end

    self:RefreshRuntimeCooldowns()
    self:StartUpdate()
end
