--[[-------------------------------------------------------------------
--  BrightQoL – BloodlustDetect Module
--
--  Deteção por dois sinais com confirmação de 2s:
--    Signal A – haste spike vs weighted rolling baseline (PollHaste)
--    Signal B – nova aura via UNIT_AURA updateInfo.addedAuras
--
--  API 12.0.1 compliant. Gated contra combat lockdown onde necessário.
-------------------------------------------------------------------]]--

local addonName, ns = ...
if not ns then return end

-- Localized globals (hot path optimization)
local C_Timer          = C_Timer
local C_UnitAuras      = C_UnitAuras
local GetHaste         = GetHaste
local GetTime          = GetTime
local InCombatLockdown = InCombatLockdown
local PlaySoundFile    = PlaySoundFile
local StopSound        = StopSound
local wipe             = wipe
local pairs            = pairs
local ipairs           = ipairs
local math_abs         = math.abs
local math_max         = math.max
local math_floor       = math.floor

---------------------------------------------------------------------------
-- Module table
---------------------------------------------------------------------------
local BLD = {}
ns.BloodlustDetect = BLD

---------------------------------------------------------------------------
-- Lust / Sated spell-ID pairs
---------------------------------------------------------------------------
local LUST_PAIRS = {
    {   2825,     57724,  "Bloodlust"           },
    {  32182,     57723,  "Heroism"             },
    {  80353,     80354,  "Time Warp"           },
    {  90355,     95809,  "Ancient Hysteria"    },
    { 264667,     95809,  "Primal Rage"         },
    { 390386,    390435,  "Fury of the Aspects" },
    { 459526,    428628,  "Harrier's Cry"       },
}

local SATED_SPELL_IDS = {}
local LUST_BUFF_IDS   = {}
for _, pair in ipairs(LUST_PAIRS) do
    LUST_BUFF_IDS[pair[1]]   = true
    SATED_SPELL_IDS[pair[2]] = true
end

---------------------------------------------------------------------------
-- Constants
---------------------------------------------------------------------------
local BASELINE_SIZE       = 8
local BASELINE_WEIGHTS    = { 1, 1, 1, 2, 2, 3, 3, 4 }  -- sum = 17
local BASELINE_WEIGHT_SUM = 17
local CONFIRM_TICK        = 0.1
local CONFIRM_TICKS       = 20   -- 2.0s window
local BL_MULTIPLIER       = 1.30
local SATED_DURATION      = 600  -- 10 minutes

---------------------------------------------------------------------------
-- Sound registry
---------------------------------------------------------------------------
local ADDON_PATH = "Interface\\AddOns\\BrightQoL\\"

BLD.soundRegistry = {
    ["INVINCIBILITY"] = {
        name       = "Invincibility",
        file       = ADDON_PATH .. "sounds\\invincibility.ogg",
        sort_order = 1,
    },
    ["MEME"] = {
        name       = "Meme",
        file       = ADDON_PATH .. "sounds\\meme.ogg",
        sort_order = 2,
    },
    ["VULPERA"] = {
        name       = "Vulpera Warrior Techno",
        file       = ADDON_PATH .. "sounds\\vulperawarrior_techno.ogg",
        sort_order = 3,
    },
    ["BLOODLUST"] = {
        name       = "Bloodlust",
        file       = ADDON_PATH .. "sounds\\bloodlust.ogg",
        sort_order = 4,
    },
    ["HEROISM"] = {
        name       = "Heroism",
        file       = ADDON_PATH .. "sounds\\heroism.ogg",
        sort_order = 5,
    },
    ["ILLIDAN"] = {
        name       = "Illidan",
        file       = ADDON_PATH .. "sounds\\illidan.ogg",
        sort_order = 6,
    },
    ["MURLOC"] = {
        name       = "Murloc",
        file       = ADDON_PATH .. "sounds\\murloc.ogg",
        sort_order = 7,
    },
    ["SUNFLOWER"] = {
        name       = "Sunflower",
        file       = ADDON_PATH .. "sounds\\Sunflower.ogg",
        sort_order = 8,
    },
    ["XALATATH"] = {
        name       = "Xal'atath",
        file       = ADDON_PATH .. "sounds\\xalatath.ogg",
        sort_order = 9,
    },
}

BLD.channelRegistry = {
    ["Master"]   = "Master",
    ["Music"]    = "Music",
    ["SFX"]      = "SFX",
    ["Ambience"] = "Ambience",
    ["Dialog"]   = "Dialog",
}

---------------------------------------------------------------------------
-- DB defaults (merged into BrightQoL SavedVariables under .bloodlust key)
---------------------------------------------------------------------------
local DEFAULTS = {
    enabled = true,
    sound   = "MEME",
    channel = "Master",
    debug   = false,
    detection = {
        fade_threshold = 115,
        sustain_needed = 2,
    },
}

---------------------------------------------------------------------------
-- DB accessor (lazy — set during Initialize)
---------------------------------------------------------------------------
local db  -- points to BrightQoL.bloodlust after init

---------------------------------------------------------------------------
-- State
---------------------------------------------------------------------------
local state = {
    Haste             = false,
    Sated             = false,
    lustActive        = false,
    preLustBaseline   = nil,
    sustainCount      = 0,
    lastTriggerTime   = nil,
    previousDebuffs   = nil,
    lastCalibHaste    = nil,
    lastCalibTime     = nil,
    missedInCombat    = nil,
    baseline          = nil,
    pollTicker        = nil,
    resumeTimer       = nil,
    confirmTimer      = nil,
    soundHandle       = nil,
    suppressSound     = true,
    eventsRegistered  = false,
}

---------------------------------------------------------------------------
-- Event frame (private to this module)
---------------------------------------------------------------------------
local eventFrame   = CreateFrame("Frame")
local handlers     = {}
local unitHandlers = {}

eventFrame:SetScript("OnEvent", function(_, event, ...)
    local h  = handlers[event]
    if h then h(event, ...) end
    local uh = unitHandlers[event]
    if uh and uh ~= h then uh(event, ...) end
end)

local function RegisterEvent(event, fn)
    handlers[event] = fn
    eventFrame:RegisterEvent(event)
end

local function UnregisterEvent(event)
    handlers[event] = nil
    if not unitHandlers[event] then
        eventFrame:UnregisterEvent(event)
    end
end

local function RegisterUnitEvent(event, fn, unit)
    unitHandlers[event] = fn
    eventFrame:RegisterUnitEvent(event, unit)
end

---------------------------------------------------------------------------
-- Logging helpers
---------------------------------------------------------------------------
local PREFIX = "|cffa855f7Bright|r|cff00cfff QoL|r |cffaaaaaa[BLD]|r "

local function Log(fmt, ...)
    DEFAULT_CHAT_FRAME:AddMessage(PREFIX .. string.format(fmt, ...))
end

---------------------------------------------------------------------------
-- Baseline helpers
---------------------------------------------------------------------------
local function weightedMean(v)
    local sum = 0
    for i = 1, BASELINE_SIZE do
        sum = sum + v[i] * BASELINE_WEIGHTS[i]
    end
    return sum / BASELINE_WEIGHT_SUM
end
BLD.weightedMean = weightedMean  -- exposed for config panel status

local function maxValue(v)
    local m = v[1] or 0
    for i = 2, BASELINE_SIZE do
        if v[i] > m then m = v[i] end
    end
    return m
end

local function shiftBaseline(v, val)
    for i = 1, BASELINE_SIZE - 1 do v[i] = v[i + 1] end
    v[BASELINE_SIZE] = val
end

local function resetBaseline()
    local h = GetHaste() or 0
    state.baseline = {}
    for i = 1, BASELINE_SIZE do state.baseline[i] = h end
end

---------------------------------------------------------------------------
-- Auto-calibration
---------------------------------------------------------------------------
local function computeBlRatio(haste)
    local factor   = 1 + haste / 100
    local postLust = (factor * BL_MULTIPLIER - 1) * 100
    return postLust / haste
end

local function autoCalibrateThresholds()
    local haste = GetHaste() or 0
    local eff   = math_max(5, haste)
    local ratio = computeBlRatio(eff)
    local spike = math_max(130, math_floor(ratio * 92))
    local jump  = math_max(125, math_floor(ratio * 88))

    db.detection.spike_threshold = spike
    db.detection.jump_threshold  = jump
    state.lastCalibHaste = haste
    state.lastCalibTime  = GetTime()

    if db.debug then
        Log("AutoCalib: haste=%.1f%%  spike=%d  jump=%d  (BL≈%.0f%%)",
            haste, spike, jump, ratio * 100)
    end
end

---------------------------------------------------------------------------
-- Confirm timer
---------------------------------------------------------------------------
local function cancelConfirmTimer()
    if state.confirmTimer and not state.confirmTimer:IsCancelled() then
        state.confirmTimer:Cancel()
    end
    state.confirmTimer = nil
end

local function confirmBloodlust()  -- forward declaration
end

local function onConfirmTick(timer)
    timer.count = timer.count - 1

    if state.Haste and state.Sated then
        confirmBloodlust()
        timer:Cancel()
        return
    end

    if timer.count <= 0 then
        if db.debug then
            Log("Confirm window expired  Haste=%s  Sated=%s",
                tostring(state.Haste), tostring(state.Sated))
        end

        if state.Sated and state.sustainCount > 0 and InCombatLockdown() then
            state.missedInCombat  = true
            state.lastTriggerTime = GetTime()
            if db.debug then Log("Flagged as missed in-combat lust") end
        end

        state.Haste        = false
        state.Sated        = false
        state.sustainCount = 0
        timer:Cancel()
    end
end

local function startConfirmTimer()
    if state.confirmTimer and not state.confirmTimer:IsCancelled() then return end
    if db.debug then Log("Confirm window opened (%.1fs)", CONFIRM_TICKS * CONFIRM_TICK) end
    state.confirmTimer       = C_Timer.NewTicker(CONFIRM_TICK, onConfirmTick, CONFIRM_TICKS)
    state.confirmTimer.count = CONFIRM_TICKS
end

---------------------------------------------------------------------------
-- Sound
---------------------------------------------------------------------------
local function playAlert()
    if state.suppressSound then return end
    local entry = BLD.soundRegistry[db.sound]
    if not entry then return end

    if state.soundHandle then
        StopSound(state.soundHandle, 500)
        state.soundHandle = nil
    end

    local ok, handle = PlaySoundFile(entry.file, db.channel)
    if ok then state.soundHandle = handle end
end

function BLD:PlayTest()
    local entry = self.soundRegistry[db.sound]
    if not entry then return end
    if state.soundHandle then StopSound(state.soundHandle, 500); state.soundHandle = nil end
    local ok, handle = PlaySoundFile(entry.file, db.channel)
    if ok then state.soundHandle = handle end
end

function BLD:StopSound()
    if state.soundHandle then
        StopSound(state.soundHandle, 500)
        state.soundHandle = nil
    end
end

---------------------------------------------------------------------------
-- Trigger / stop
---------------------------------------------------------------------------
confirmBloodlust = function()
    if state.lustActive then return end
    cancelConfirmTimer()

    state.lustActive      = true
    state.lastTriggerTime = GetTime()
    state.sustainCount    = 0

    playAlert()

    if db.debug then Log("|cff00ff00BLOODLUST CONFIRMED!|r") end
end

local function pauseTicker()
    if state.pollTicker then state.pollTicker:Cancel(); state.pollTicker = nil end
    if state.resumeTimer then state.resumeTimer:Cancel(); state.resumeTimer = nil end

    local elapsed   = state.lastTriggerTime and (GetTime() - state.lastTriggerTime) or 0
    local remaining = math_max(1, SATED_DURATION - elapsed)

    if db.debug then Log("Poll ticker paused for %.0fs (Sated)", remaining) end

    state.resumeTimer = C_Timer.NewTimer(remaining, function()
        if state.pollTicker then return end
        resetBaseline()
        state.pollTicker = C_Timer.NewTicker(0.25, function() BLD:PollHaste() end)
        if db.debug then Log("Poll ticker resumed") end
    end)
end

local LUST_END_SOUND = ADDON_PATH .. "sounds\\lust_end.ogg"

local function stopBloodlust()
    state.Haste           = false
    state.Sated           = false
    state.preLustBaseline = nil
    state.lustActive      = false
    state.sustainCount    = 0
    cancelConfirmTimer()
    pauseTicker()
    PlaySoundFile(LUST_END_SOUND, db and db.channel or "Master")
end

---------------------------------------------------------------------------
-- Aura helpers
---------------------------------------------------------------------------
local function playerHasSated()
    for spellId in pairs(SATED_SPELL_IDS) do
        if C_UnitAuras.GetPlayerAuraBySpellID(spellId) then return true end
    end
    return false
end

local function playerHasLustBuff()
    for spellId in pairs(LUST_BUFF_IDS) do
        if C_UnitAuras.GetPlayerAuraBySpellID(spellId) then return true end
    end
    return false
end

local function getDebuffSnapshot()
    return C_UnitAuras.GetUnitAuras("player", "HARMFUL")
end

local prevSetCache = {}
local function debuffGained(prev, curr)
    if not prev or not curr then return false end
    wipe(prevSetCache)
    for _, aura in ipairs(prev) do
        if aura.auraInstanceID then prevSetCache[aura.auraInstanceID] = true end
    end
    for _, aura in ipairs(curr) do
        if aura.auraInstanceID and not prevSetCache[aura.auraInstanceID] then
            return true
        end
    end
    return false
end

---------------------------------------------------------------------------
-- Haste spike check
---------------------------------------------------------------------------
local function checkHasteSpike(current, m, mv)
    local det            = db.detection
    local spikeThreshold = det.spike_threshold / 100
    local jumpThreshold  = det.jump_threshold  / 100

    if m <= 0 then
        return (current > 0), (current > 0) and 999 or 0, (current > 0) and 999 or 0
    end
    if mv <= 0 then mv = m end

    local ratio = current / m
    local jump  = current / mv
    return (ratio >= spikeThreshold) and (jump >= jumpThreshold), ratio, jump
end

---------------------------------------------------------------------------
-- Poll haste (runs every 0.25s)
---------------------------------------------------------------------------
function BLD:PollHaste()
    if not db.enabled then return end

    local v = state.baseline
    if not v then return end

    local current                = GetHaste() or 0
    local m                      = weightedMean(v)
    local mv                     = maxValue(v)
    local maybeLust, ratio, jump = checkHasteSpike(current, m, mv)

    -- Freeze guard while active
    if state.Haste or state.lustActive then
        if state.lustActive then
            local bl = state.preLustBaseline
            if bl and bl > 0 then
                local fadeRatio     = current / bl
                local fadeThreshold = db.detection.fade_threshold / 100
                if fadeRatio < fadeThreshold then
                    stopBloodlust()
                else
                    return
                end
            elseif bl and bl <= 0 then
                if current <= 0 then stopBloodlust() else return end
            else
                return
            end
        else
            return
        end

    elseif state.Sated then
        -- Reverse-BL verification
        local currentFactor = 1 + current / 100
        local preBLFactor   = currentFactor / BL_MULTIPLIER
        local preBLHaste    = (preBLFactor - 1) * 100
        local matchFound    = false

        for i = 1, BASELINE_SIZE do
            local diff = math_abs(v[i] - preBLHaste)
            if diff < 3 or (preBLHaste > 0 and diff / preBLHaste < 0.05) then
                matchFound = true
                break
            end
        end

        if matchFound then
            state.sustainCount = (state.sustainCount or 0) + 1
            local needed = db.detection.sustain_needed or 2

            if state.sustainCount >= needed then
                if db.debug then
                    Log("Reverse-BL confirmed (%d/%d)  cur=%.2f  preBL=%.2f",
                        state.sustainCount, needed, current, preBLHaste)
                end
                state.preLustBaseline = preBLHaste
                state.Haste           = true
                startConfirmTimer()
            end
        else
            state.sustainCount = 0
        end
        return

    else
        -- Normal spike detection
        if maybeLust then
            state.sustainCount = (state.sustainCount or 0) + 1
            local needed = db.detection.sustain_needed or 2

            if state.sustainCount >= needed then
                if db.debug then
                    Log("Haste spike confirmed (%d/%d)  cur=%.2f  mean=%.2f  ratio=%.2f",
                        state.sustainCount, needed, current, m, ratio)
                end
                state.preLustBaseline = m
                state.Haste           = true

                -- Signal B cross-check
                if not state.Sated and state.previousDebuffs then
                    local curr   = getDebuffSnapshot()
                    local gained = debuffGained(state.previousDebuffs, curr)
                    state.previousDebuffs = curr
                    if gained then
                        if db.debug then Log("Signal B cross-check: new debuff found") end
                        state.Sated = true
                    end
                end

                startConfirmTimer()
                return
            else
                if db.debug then
                    Log("Spike candidate (%d/%d)  ratio=%.2f", state.sustainCount, needed, ratio)
                end
                return
            end
        else
            if (state.sustainCount or 0) > 0 then state.sustainCount = 0 end
        end
    end

    -- Normal baseline update
    shiftBaseline(v, current)

    -- Mid-combat recalibration on drift
    if state.lastCalibHaste then
        local drift      = math_abs(current - state.lastCalibHaste)
        local cooledDown = not state.lastCalibTime or (GetTime() - state.lastCalibTime > 2.0)
        if drift > 5 and cooledDown then autoCalibrateThresholds() end
    end
end

---------------------------------------------------------------------------
-- UNIT_AURA handler
---------------------------------------------------------------------------
local function onUnitAura(event, unit, updateInfo)
    if InCombatLockdown() then
        if not db.enabled then return end
        if state.lustActive then return end

        local hasNewAura = updateInfo
            and updateInfo.addedAuras
            and #updateInfo.addedAuras > 0

        if hasNewAura and not state.Sated then
            if db.debug then Log("UNIT_AURA: new aura(s) detected — Sated=true") end
            state.Sated = true
            startConfirmTimer()
        end
    else
        BLD:CheckOutOfCombatLust()
    end
end

---------------------------------------------------------------------------
-- Out-of-combat fallback
---------------------------------------------------------------------------
function BLD:CheckOutOfCombatLust()
    if not db.enabled then return end
    if InCombatLockdown() then return end
    if state.lustActive then return end
    if state.lastTriggerTime and (GetTime() - state.lastTriggerTime) < SATED_DURATION then return end
    if not state.baseline then return end

    if playerHasLustBuff() and playerHasSated() then
        if db.debug then Log("Out-of-combat lust detected") end
        local lustHaste  = weightedMean(state.baseline)
        local lustFactor = 1 + lustHaste / 100
        local preLust    = (lustFactor / BL_MULTIPLIER - 1) * 100
        state.preLustBaseline = preLust
        state.Haste           = true
        state.Sated           = true
        confirmBloodlust()
    end
end

---------------------------------------------------------------------------
-- Combat events
---------------------------------------------------------------------------
local function onRegenEnabled()
    local needsOOCCheck = false

    if state.lustActive then
        stopBloodlust()
    elseif state.confirmTimer and not state.confirmTimer:IsCancelled() then
        if not state.Sated and playerHasSated() then
            if db.debug then Log("REGEN_ENABLED: filled Signal B via spell-ID") end
            state.Sated = true
        end
    else
        state.Haste           = false
        state.Sated           = false
        state.preLustBaseline = nil
        state.sustainCount    = 0
        cancelConfirmTimer()
        needsOOCCheck         = true
    end

    state.previousDebuffs = nil

    if needsOOCCheck and not state.missedInCombat then
        C_Timer.After(0.3, function()
            if db then BLD:CheckOutOfCombatLust() end
        end)
    end

    state.missedInCombat = nil
end

local function onRegenDisabled()
    state.Haste          = false
    state.Sated          = false
    state.sustainCount   = 0
    state.missedInCombat = nil
    state.previousDebuffs = getDebuffSnapshot()
    cancelConfirmTimer()

    if state.pollTicker then state.pollTicker:Cancel(); state.pollTicker = nil end
    if state.resumeTimer then state.resumeTimer:Cancel(); state.resumeTimer = nil end

    resetBaseline()
    state.pollTicker = C_Timer.NewTicker(0.25, function() BLD:PollHaste() end)
    autoCalibrateThresholds()
end

local function onEnteringWorld()
    if not state.eventsRegistered then return end

    state.Haste           = false
    state.Sated           = false
    state.lustActive      = false
    state.preLustBaseline = nil
    state.sustainCount    = 0
    state.missedInCombat  = nil
    cancelConfirmTimer()

    if state.pollTicker then state.pollTicker:Cancel(); state.pollTicker = nil end

    C_Timer.After(2.0, function()
        if not db then return end
        resetBaseline()
        state.pollTicker = C_Timer.NewTicker(0.25, function() BLD:PollHaste() end)
    end)
end

local function onReadyCheck()
    if state.pollTicker then return end
    if db.debug then Log("Ready check — resuming detection") end
    if state.resumeTimer then state.resumeTimer:Cancel(); state.resumeTimer = nil end
    resetBaseline()
    state.pollTicker = C_Timer.NewTicker(0.25, function() BLD:PollHaste() end)
end

---------------------------------------------------------------------------
-- Initialize (called from ADDON_LOADED via BrightQoL Core)
---------------------------------------------------------------------------
function BLD:Initialize()
    -- Ensure BrightQoL.bloodlust exists with defaults
    BrightQoL.bloodlust = BrightQoL.bloodlust or {}
    local saved = BrightQoL.bloodlust

    -- Apply defaults
    if saved.enabled   == nil then saved.enabled   = DEFAULTS.enabled   end
    if saved.sound     == nil then saved.sound      = DEFAULTS.sound     end
    if saved.channel   == nil then saved.channel    = DEFAULTS.channel   end
    if saved.debug     == nil then saved.debug      = DEFAULTS.debug     end
    saved.detection = saved.detection or {}
    local det = saved.detection
    if det.fade_threshold == nil then det.fade_threshold = DEFAULTS.detection.fade_threshold end
    if det.sustain_needed == nil then det.sustain_needed = DEFAULTS.detection.sustain_needed end

    db = saved

    -- Delay startup so UI and initial aura state settle
    C_Timer.After(2.0, function()
        RegisterEvent("PLAYER_REGEN_ENABLED",  onRegenEnabled)
        RegisterEvent("PLAYER_REGEN_DISABLED", onRegenDisabled)
        RegisterEvent("PLAYER_ENTERING_WORLD", onEnteringWorld)
        RegisterEvent("READY_CHECK",           onReadyCheck)
        RegisterUnitEvent("UNIT_AURA", onUnitAura, "player")

        resetBaseline()
        state.previousDebuffs = getDebuffSnapshot()
        state.eventsRegistered = true

        autoCalibrateThresholds()

        state.pollTicker = C_Timer.NewTicker(0.25, function() BLD:PollHaste() end)

        C_Timer.After(1.0, function()
            state.suppressSound = false
        end)
    end)
end

---------------------------------------------------------------------------
-- Status helper (used by config panel)
---------------------------------------------------------------------------
function BLD:GetStatus()
    return {
        enabled       = db and db.enabled,
        lustActive    = state.lustActive,
        haste         = state.Haste,
        sated         = state.Sated,
        sustainCount  = state.sustainCount,
        currentHaste  = GetHaste() or 0,
        baselineMean  = state.baseline and weightedMean(state.baseline) or 0,
        tickerRunning = state.pollTicker ~= nil,
        lastTrigger   = state.lastTriggerTime,
        db            = db,
    }
end

---------------------------------------------------------------------------
-- Hook into BrightQoL ADDON_LOADED lifecycle
---------------------------------------------------------------------------
local loader = CreateFrame("Frame")
loader:RegisterEvent("ADDON_LOADED")
loader:SetScript("OnEvent", function(self, event, name)
    if name == addonName then
        BLD:Initialize()
        self:UnregisterEvent("ADDON_LOADED")
    end
end)
