-- MSUF_GF_RangeThreat.lua
-- Group Frame range-fade and threat-state runtime.

local _, ns = ...
ns = ns or (_G.MSUF_NS) or {}
_G.MSUF_NS = ns

local GF = ns.GF
if not GF then return end

local issecretvalue = _G.issecretvalue
local UnitExists = _G.UnitExists
local UnitIsConnected = _G.UnitIsConnected
local UnitHealth = _G.UnitHealth
local UnitInRange = _G.UnitInRange
local UnitGUID = _G.UnitGUID
local UnitFullName = _G.UnitFullName
local UnitName = _G.UnitName
local UnitGroupRolesAssigned = _G.UnitGroupRolesAssigned
local UnitThreatSituation = _G.UnitThreatSituation
local IsInGroup = _G.IsInGroup
local IsInRaid = _G.IsInRaid
local IsInInstance = _G.IsInInstance
local InCombatLockdown = _G.InCombatLockdown
local C_Timer = _G.C_Timer
local CreateFrame = _G.CreateFrame

local function _RuntimeEnabledForFrame(f)
    local fn = GF._RuntimeEnabledForFrame
    if type(fn) == "function" then return fn(f) end
    return f ~= nil
end

local _GF_RefreshBorder = GF.RefreshBorder or _G.MSUF_GF_RefreshBorder or function() end
local UpdateStatusText = GF.UpdateStatusText or _G.MSUF_GF_UpdateStatus or function() end

-- Range fade (1:1 EQoL pattern)
-- Secret-safe: NEVER compare/type()/conditional on inRange.
-- Pass raw value to SetAlphaFromBoolean (C-side accepts secrets).
-- 1:1 EQoL GF:UpdateRange pattern Ã¢â‚¬â€ NO extra UnitPhaseReason/UnitIsVisible.
------------------------------------------------------------------------

-- EQoL UnsecretBool equivalent
local function _UnsecretBool(value)
    if issecretvalue and issecretvalue(value) then return nil end
    return value
end

local _playerGUID
local _playerNameKey

local function _GF_SafeGUID(unit)
    if not (unit and UnitGUID) then return nil end
    local guid = UnitGUID(unit)
    if guid and not (issecretvalue and issecretvalue(guid)) then
        return guid
    end
    return nil
end

local function _GF_NameKey(unit)
    if not unit then return nil end
    if UnitFullName then
        local name, realm = UnitFullName(unit)
        if name and not (issecretvalue and issecretvalue(name)) then
            if realm and realm ~= "" and not (issecretvalue and issecretvalue(realm)) then
                return name .. "-" .. realm
            end
            return name
        end
    elseif UnitName then
        local name, realm = UnitName(unit)
        if name and not (issecretvalue and issecretvalue(name)) then
            if realm and realm ~= "" and not (issecretvalue and issecretvalue(realm)) then
                return name .. "-" .. realm
            end
            return name
        end
    end
    return nil
end

local function _GF_IsSelfUnit(f, unit)
    if unit == "player" then return true end
    if not unit then return false end

    if f and f._msufGFRangeFadeSelfUnit == unit and f._msufGFRangeFadeSelfResolved == true then
        return f._msufGFRangeFadeSelf == true
    end

    local resolved, isSelf = false, false
    local pg = _playerGUID or _GF_SafeGUID("player")
    if pg then
        _playerGUID = pg
        local ug = _GF_SafeGUID(unit)
        if ug then
            resolved = true
            isSelf = (ug == pg)
        end
    end

    if not resolved then
        local pk = _playerNameKey or _GF_NameKey("player")
        if pk then
            _playerNameKey = pk
            local uk = _GF_NameKey(unit)
            if uk then
                resolved = true
                isSelf = (uk == pk)
            end
        end
    end

    if f and resolved then
        f._msufGFRangeFadeSelfUnit = unit
        f._msufGFRangeFadeSelf = isSelf
        f._msufGFRangeFadeSelfResolved = true
    end
    return isSelf
end

local function _NormalizeRangeFadeLayerMode(mode)
    if mode == 2 or mode == "health" or mode == "hp" or mode == "hpbar" then
        return "health"
    end
    return "frame"
end

local function _GF_GetFrameAlpha(kind, conf)
    local fn = GF.GetEffectiveFrameAlpha
    if type(fn) == "function" then return fn(kind, conf) end
    return 1
end

local function _GF_ApplyFrameAlpha(f, kind, conf)
    local target = (f and f.barGroup) or f
    if target and target.SetAlpha then
        local c = f._c
        local a = c and c.frameAlpha
        if type(a) ~= "number" then
            a = _GF_GetFrameAlpha(kind, conf or GF.GetConf(kind))
        end
        target:SetAlpha(a)
    end
end

local function _ClearHealthRangeFade(f, kind)
    if not f then return end
    if f._msufGFHealthAlphaDynamic or f._msufGFHealthAlphaMul or type(f._msufGFHealthAlphaBool) ~= "nil" then
        f._msufGFHealthAlphaDynamic = nil
        f._msufGFHealthAlphaMul = nil
        f._msufGFHealthAlphaBool = nil
        f._msufGFHealthAlphaFalseMul = nil
        if GF.ApplyHealthBarAlpha then
            GF.ApplyHealthBarAlpha(f, kind or f._msufGFKind or "party")
        elseif f.health and f.health.SetAlpha then
            f.health:SetAlpha(1)
        end
    end
end

local function _ApplyHealthRangeFade(f, kind, boolValue, fadeMul, numericMul)
    if not f then return end
    local m = tonumber(fadeMul) or 1
    if m < 0 then m = 0 elseif m > 1 then m = 1 end

    f._msufGFHealthAlphaDynamic = true
    f._msufGFHealthAlphaFalseMul = m
    if type(boolValue) ~= "nil" then
        f._msufGFHealthAlphaBool = boolValue
        f._msufGFHealthAlphaMul = nil
    else
        local nm = tonumber(numericMul)
        if nm == nil then nm = m end
        if nm < 0 then nm = 0 elseif nm > 1 then nm = 1 end
        f._msufGFHealthAlphaBool = nil
        f._msufGFHealthAlphaMul = nm
    end

    _GF_ApplyFrameAlpha(f, kind or f._msufGFKind or "party")
    if GF.ApplyHealthBarAlpha then
        GF.ApplyHealthBarAlpha(f, kind or f._msufGFKind or "party")
    elseif f.health and f.health.SetAlpha and type(boolValue) == "nil" then
        f.health:SetAlpha(f._msufGFHealthAlphaMul or 1)
    end
end

local ApplyRangeFade
do
    ApplyRangeFade = function(f, unit, inRange)
        local c = f._c
        local kind = f._msufGFKind or "party"
        if not c and GF.BuildFrameCache then GF.BuildFrameCache(f); c = f._c end
        if f._msufGFRangeFadeUnit ~= unit then
            f._msufGFRangeFadeUnit = unit
            f._msufGFRangeFadeSelfUnit = nil
            f._msufGFRangeFadeSelf = nil
            f._msufGFRangeFadeSelfResolved = nil
        end
        local conf
        if not c then conf = GF.GetConf(kind) end
        local frameAlpha = (c and c.frameAlpha) or _GF_GetFrameAlpha(kind, conf)
        if (c and not c.rfEn) or (conf and conf.rangeFadeEnabled == false) then
            f._msufGFRangeFadeUnit = nil
            _ClearHealthRangeFade(f, kind)
            local target = (f and f.barGroup) or f
            if target and target.SetAlpha then target:SetAlpha(frameAlpha) end
            return
        end
        local fadeAlpha = (c and c.rfAlpha) or (conf and conf.rangeFadeAlpha) or 0.4
        local hpMode = ((c and c.rfLayerMode) or (conf and _NormalizeRangeFadeLayerMode(conf.rangeFadeLayerMode)) or "frame") == "health"

        if IsInGroup and IsInRaid then
            local inGroup = IsInGroup()
            local inRaid = IsInRaid()
            if not inGroup and not inRaid then
                f._msufGFRangeFadeUnit = nil
                _ClearHealthRangeFade(f, kind)
                local target = (f and f.barGroup) or f
                if target and target.SetAlpha then target:SetAlpha(frameAlpha) end
                return
            end
        end

        local connected = unit and UnitIsConnected and _UnsecretBool(UnitIsConnected(unit)) or nil
        if connected == false then
            f._msufGFRangeFadeUnit = nil
            local offA = (c and c.offAlpha) or (conf and conf.offlineAlpha) or fadeAlpha
            if hpMode then
                _ApplyHealthRangeFade(f, kind, nil, offA, offA)
            else
                _ClearHealthRangeFade(f, kind)
                local target = (f and f.barGroup) or f
                if target and target.SetAlpha then target:SetAlpha(frameAlpha * offA) end
            end
            return
        end

        if not hpMode then
            _ClearHealthRangeFade(f, kind)
        end
        if _GF_IsSelfUnit(f, unit) then
            inRange = true
        elseif inRange == nil and unit and UnitInRange then
            inRange = UnitInRange(unit)
        end
        if type(inRange) ~= "nil" then
            if hpMode then
                _ApplyHealthRangeFade(f, kind, inRange, fadeAlpha)
            else
                local target = (f and f.barGroup) or f
                if target and target.SetAlphaFromBoolean then
                    target:SetAlphaFromBoolean(inRange, frameAlpha, frameAlpha * fadeAlpha)
                elseif target and target.SetAlpha then
                    local boolValue = _UnsecretBool(inRange)
                    if type(boolValue) ~= "nil" then
                        target:SetAlpha((boolValue and frameAlpha) or (frameAlpha * fadeAlpha))
                    end
                end
            end
        else
            _ClearHealthRangeFade(f, kind)
            local target = (f and f.barGroup) or f
            if target and target.SetAlpha then target:SetAlpha(frameAlpha) end
        end
    end
end

local RANGE_SETTLE_INTERVAL = 0.25
local RANGE_SETTLE_PASSES = 4
local _rangeSettleTicker
local _rangeSettlePasses
local _rangeSettlePendingCombat

local function _GF_InCombat()
    return _G.MSUF_InCombat == true or (InCombatLockdown and InCombatLockdown()) and true or false
end

local function _GF_InInstance()
    if not IsInInstance then return false end
    local inInstance = IsInInstance()
    if issecretvalue and issecretvalue(inInstance) then return false end
    return inInstance == true or inInstance == 1
end

local function _GF_CancelRangeSettleTicker()
    local ticker = _rangeSettleTicker
    if ticker and ticker.Cancel then ticker:Cancel() end
    _rangeSettleTicker = nil
    _rangeSettlePasses = nil
end

local function _GF_FrameWantsRangeRefresh(f)
    if not (f and _RuntimeEnabledForFrame(f)) then return false end
    local unit = f.unit
    if not unit then return false end
    if UnitExists then
        local exists = UnitExists(unit)
        if issecretvalue and issecretvalue(exists) then return false end
        if not exists then return false end
    end

    local c = f._c
    if not c and GF.BuildFrameCache then
        GF.BuildFrameCache(f)
        c = f._c
    end
    if c and c.rfEn then return true end

    local kind = f._msufGFKind or "party"
    local conf = GF.GetConf and GF.GetConf(kind)
    return conf and conf.rangeFadeEnabled ~= false
end

local function _GF_RefreshRangeFrame(f)
    if not _GF_FrameWantsRangeRefresh(f) then return 0 end
    ApplyRangeFade(f, f.unit)
    return 1
end

function GF.RefreshRangeFade()
    local refreshed = 0
    if type(GF.ForEachFrame) == "function" then
        GF.ForEachFrame(function(f)
            refreshed = refreshed + _GF_RefreshRangeFrame(f)
        end)
        return refreshed
    end

    local list = GF.frameList
    if list then
        for i = 1, #list do
            refreshed = refreshed + _GF_RefreshRangeFrame(list[i])
        end
        return refreshed
    end

    local frames = GF.frames
    if frames then
        for f in pairs(frames) do
            refreshed = refreshed + _GF_RefreshRangeFrame(f)
        end
    end
    return refreshed
end

function GF.ScheduleRangeFadeSettleRefresh(reason)
    if not _GF_InInstance() then
        _rangeSettlePendingCombat = nil
        _GF_CancelRangeSettleTicker()
        return false
    end
    if _GF_InCombat() then
        _rangeSettlePendingCombat = reason or true
        _GF_CancelRangeSettleTicker()
        return false
    end

    if not (C_Timer and C_Timer.NewTicker) then
        GF.RefreshRangeFade()
        return true
    end

    _GF_CancelRangeSettleTicker()
    _rangeSettlePendingCombat = nil
    _rangeSettlePasses = 0
    GF.RefreshRangeFade()
    _rangeSettleTicker = C_Timer.NewTicker(RANGE_SETTLE_INTERVAL, function()
        _rangeSettlePasses = (_rangeSettlePasses or 0) + 1
        if not _GF_InInstance() then
            _GF_CancelRangeSettleTicker()
            return
        end
        if _GF_InCombat() then
            _rangeSettlePendingCombat = true
            _GF_CancelRangeSettleTicker()
            return
        end
        GF.RefreshRangeFade()
        if (_rangeSettlePasses or 0) >= RANGE_SETTLE_PASSES then
            _GF_CancelRangeSettleTicker()
        end
    end)
    return true
end

if CreateFrame then
    local eventFrame = CreateFrame("Frame")
    eventFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
    eventFrame:RegisterEvent("ZONE_CHANGED_NEW_AREA")
    eventFrame:RegisterEvent("GROUP_ROSTER_UPDATE")
    eventFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
    eventFrame:SetScript("OnEvent", function(_, event)
        if event == "PLAYER_REGEN_ENABLED" then
            if _rangeSettlePendingCombat then
                local reason = _rangeSettlePendingCombat
                _rangeSettlePendingCombat = nil
                GF.ScheduleRangeFadeSettleRefresh(reason)
            end
            return
        end
        GF.ScheduleRangeFadeSettleRefresh(event)
    end)
end

function GF.RefreshGroupAlphas()
    local frames = GF.frames
    if frames then
        local list = GF.frameList
        if list then
            for i = 1, #list do
                local f = list[i]
                if f and _RuntimeEnabledForFrame(f) then
                    local kind = f._msufGFKind or "party"
                    if GF.BuildFrameCache then GF.BuildFrameCache(f) end
                    if GF.ApplyHealthBarAlpha then GF.ApplyHealthBarAlpha(f, kind) end
                    if GF.ApplyPowerBarAlpha then GF.ApplyPowerBarAlpha(f, kind) end
                    if GF.ApplyBackgroundAlpha then GF.ApplyBackgroundAlpha(f, kind) end
                    if f.unit and UnitExists(f.unit) then
                        ApplyRangeFade(f, f.unit)
                    else
                        _GF_ApplyFrameAlpha(f, kind)
                    end
                end
            end
        else
            for f in pairs(frames) do
                if f and _RuntimeEnabledForFrame(f) then
                    local kind = f._msufGFKind or "party"
                    if GF.BuildFrameCache then GF.BuildFrameCache(f) end
                    if GF.ApplyHealthBarAlpha then GF.ApplyHealthBarAlpha(f, kind) end
                    if GF.ApplyPowerBarAlpha then GF.ApplyPowerBarAlpha(f, kind) end
                    if GF.ApplyBackgroundAlpha then GF.ApplyBackgroundAlpha(f, kind) end
                    if f.unit and UnitExists(f.unit) then
                        ApplyRangeFade(f, f.unit)
                    else
                        _GF_ApplyFrameAlpha(f, kind)
                    end
                end
            end
        end
    end
    -- Runtime alpha transitions are already applied above. Do not dirty the
    -- visual pipeline here: in combat, MarkAllDirty promotes to a post-combat
    -- RefreshVisuals fan-out, turning alpha-only state changes into full
    -- frame/aura refreshes.
end

local function _GF_ShouldApplyRangeOrFrameAlpha(f, c, kind)
    if c and c.rfEn then return true end
    if f and f._msufGFHealthAlphaDynamic then return true end
    local cachedAlpha = c and c.frameAlpha
    if type(cachedAlpha) == "number" then return cachedAlpha < 0.999 end
    local fn = GF.GetEffectiveFrameAlpha
    if type(fn) ~= "function" then return false end
    kind = kind or (f and f._msufGFKind) or "party"
    local a = fn(kind, GF.GetConf(kind))
    return type(a) == "number" and a < 0.999
end

------------------------------------------------------------------------
function GF._RefreshAggroConsumers(f, unit, c)
    _GF_RefreshBorder(f, unit)
    if c and c.ciThreat and GF.UpdateCornerIndicators then
        f._msufCILastAt = nil
        GF.UpdateCornerIndicators(f, unit)
    end
end

local function UpdateAggro(f, unit)
    local kind = f._msufGFKind or "party"
    local prevLevel = f._msufGFAggroLevel
    -- PERF: pre-resolved flags on _c cache (populated in BuildFrameCache).
    local c = f._c
    if not c and GF.BuildFrameCache then GF.BuildFrameCache(f); c = f._c end

    local testMode = (_G.MSUF_BorderTestModesActive == true) and _G.MSUF_AggroBorderTestMode
    -- Scope filtering
    if testMode then
        local testScope = _G.MSUF_AggroBorderTestScope or "shared"
        if testScope ~= "shared" then
            local scopeKind = (testScope == "party" or testScope == "gf_party") and "party"
                or (testScope == "raid" or testScope == "gf_raid") and "raid"
                or (testScope == "mythicraid" or testScope == "gf_mythicraid") and "mythicraid" or nil
            if scopeKind ~= kind then testMode = false end
        end
    end
    local wantsAggroState = c and (c.aggroEn or c.ciThreat)
    if ((not wantsAggroState) or not unit) and not testMode then
        if prevLevel ~= nil then f._msufGFAggroLevel = nil; GF._RefreshAggroConsumers(f, unit, c) end
        return
    end

    if not testMode then
        if not UnitExists(unit) then
            if prevLevel ~= nil then f._msufGFAggroLevel = nil; GF._RefreshAggroConsumers(f, unit, c) end
            return
        end
        local aggroMode = (c and c.aggroMode) or "ALL"
        if aggroMode ~= "ALL" then
            local role = UnitGroupRolesAssigned and UnitGroupRolesAssigned(unit)
            if aggroMode == "HEALER_ONLY" and role ~= "HEALER" then
                if prevLevel ~= nil then f._msufGFAggroLevel = nil; GF._RefreshAggroConsumers(f, unit, c) end
                return
            end
            if aggroMode == "TANK_ONLY" and role ~= "TANK" then
                if prevLevel ~= nil then f._msufGFAggroLevel = nil; GF._RefreshAggroConsumers(f, unit, c) end
                return
            end
        end
        local status = UnitThreatSituation and UnitThreatSituation(unit)
        if issecretvalue and issecretvalue(status) then
            -- Secret: can't diff-gate, but only refresh if we had aggro before
            if prevLevel ~= nil then f._msufGFAggroLevel = nil; GF._RefreshAggroConsumers(f, unit, c) end
            return
        end
        local s = tonumber(status)
        if not s or s < 1 then
            if prevLevel ~= nil then f._msufGFAggroLevel = nil; GF._RefreshAggroConsumers(f, unit, c) end
            return
        end
        -- Diff-gate: only refresh border when threat level actually changes
        if s == prevLevel then return end
        f._msufGFAggroLevel = s
    else
        f._msufGFAggroLevel = 3
    end
    GF._RefreshAggroConsumers(f, unit, c)
end
GF._UpdateAggro = UpdateAggro

------------------------------------------------------------------------

GF.ApplyRangeFade = ApplyRangeFade
GF.ShouldApplyRangeOrFrameAlpha = _GF_ShouldApplyRangeOrFrameAlpha
GF.UpdateAggro = UpdateAggro
GF._UpdateAggro = UpdateAggro
if GF._UnitDispatch then
    GF._UnitDispatch.UNIT_IN_RANGE_UPDATE = function(f, u, inRange)
        local c = f and f._c
        if c and c.statusTextEn then
            local state = f._msufGFStatusState or 0
            if state ~= 0 then
                UpdateStatusText(f, u)
            elseif UnitHealth then
                local hp = UnitHealth(u)
                if not (issecretvalue and issecretvalue(hp)) and hp == 0 then
                    UpdateStatusText(f, u)
                end
            end
        end
        ApplyRangeFade(f, u, inRange)
    end
    GF._UnitDispatch.UNIT_THREAT_SITUATION_UPDATE = UpdateAggro
    GF._UnitDispatch.UNIT_THREAT_LIST_UPDATE = UpdateAggro
    GF._UnitDispatch.UNIT_PHASE = function(f, u)
        local c = f and f._c
        if c and c.phaseEn then
            local updateAll = _G.MSUF_GF_UpdateAll
            if type(updateAll) == "function" then return updateAll(f, u) end
        end
        ApplyRangeFade(f, u)
    end
    GF._UnitDispatch.UNIT_CTR_OPTIONS = ApplyRangeFade
    GF._UnitDispatch.UNIT_OTHER_PARTY_CHANGED = ApplyRangeFade
end
_G.MSUF_GF_UpdateAggro = UpdateAggro
