--- UnitFrames/Engine/Group/MSUF_UF_Group_Visuals.lua
--- Group-only visual runtime elements.
---
--- This file handles target/focus edge indicators, dispel/debuff visuals,
--- health fade/dead-background behavior, and per-GUID indicator dispatch for
--- group frames. It is an element implementation registered with the UF engine;
--- header creation, roster scanning, and DB compilation live elsewhere.

local addonName, MSUF = ...
MSUF = MSUF or _G.MSUF_NS or _G.MSUF or {}
_G.MSUF = MSUF

local UF = MSUF.UF

if not (UF and UF.RegisterElement) then return end

local CreateFrame = _G.CreateFrame

local function SetShown(region, show)
  if not region then return end
  show = show == true
  if region._msufGFVisualShown == show then return end
  region:SetShown(show)
  region._msufGFVisualShown = show
end
local UnitHealth = UnitHealth
local UnitHealthMax = UnitHealthMax
local UnitHealthPercent = UnitHealthPercent
local UnitGUID = UnitGUID
local UnitIsConnected = _G.UnitIsConnected
local tonumber = tonumber
local type = type
local max = math.max
local floor = math.floor
local GetTime = _G.GetTime
local issecretvalue = _G.issecretvalue or function(_) return false end
local IsUnitToken = UF.IsUnitToken or function(unit)
  return issecretvalue(unit) ~= true and type(unit) == "string" and unit ~= ""
end
local ReadConnectedCached = UF.ReadConnectedCached or function(_, unit)
  if not UnitIsConnected then return true, true end
  local connected = UnitIsConnected(unit)
  if issecretvalue(connected) == true or connected == nil then return true, false end
  return connected == true or connected == 1, true
end
local ReadDeadCached = UF.ReadDeadCached or function(_, unit)
  if not (_G.UnitIsDeadOrGhost or _G.UnitIsDead) then return false, true end
  local dead = _G.UnitIsDeadOrGhost and _G.UnitIsDeadOrGhost(unit) or nil
  if (issecretvalue(dead) == true or dead == nil) and _G.UnitIsDead then
    dead = _G.UnitIsDead(unit)
  end
  if issecretvalue(dead) == true or dead == nil then return false, false end
  return dead == true or dead == 1, true
end
local Clamp01 = UF.Clamp01 or function(value, fallback)
  value = tonumber(value)
  if value == nil then value = fallback end
  value = value or 0
  if value < 0 then return 0 end
  if value > 1 then return 1 end
  return value
end

local EMPTY_EVENTS = {}
local VISUAL_HEALTH_EVENTS = { "UNIT_HEALTH", "UNIT_MAXHEALTH" }
local VISUAL_HEALTH_FLAGS_EVENTS = { "UNIT_HEALTH", "UNIT_MAXHEALTH", "UNIT_FLAGS" }
local VISUAL_OFFLINE_EVENTS = { "UNIT_CONNECTION" }
local VISUAL_FLAGS_EVENTS = { "UNIT_FLAGS" }
local VISUAL_FLAGS_OFFLINE_EVENTS = { "UNIT_CONNECTION", "UNIT_FLAGS" }
local VISUAL_HEALTH_OFFLINE_EVENTS = { "UNIT_HEALTH", "UNIT_MAXHEALTH", "UNIT_CONNECTION", "UNIT_FLAGS" }
local EDGE_KEYS = { "top", "bottom", "left", "right" }
local HEALTH_FADE_SECRET_THROTTLE = 0.1

local function SetAlphaCached(region, alpha, key)
  if not (region and region.SetAlpha) then return end
  alpha = Clamp01(alpha, 1)
  key = key or "_msufGFVisualAlpha"
  if region[key] ~= alpha then
    region:SetAlpha(alpha)
    region[key] = alpha
  end
end

local function SetAlphaFromBoolean(region, value, alphaIfTrue, alphaIfFalse, key)
  if not (region and region.SetAlphaFromBoolean) then
    return false
  end
  region:SetAlphaFromBoolean(value, Clamp01(alphaIfTrue, 1), Clamp01(alphaIfFalse, 1))
  if key then
    region[key] = nil
  end
  return true
end

local function PlainBool(value)
  if issecretvalue(value) == true then
    return nil
  end
  if value == true or value == 1 then
    return true
  end
  if value == false or value == 0 then
    return false
  end
  return nil
end

local function SetHeightCached(region, height, key)
  if not (region and region.SetHeight) then return end
  height = tonumber(height) or 0
  key = key or "_msufGFVisualHeight"
  if region[key] ~= height then
    region:SetHeight(height)
    region[key] = height
  end
end

local function SetWidthCached(region, width, key)
  if not (region and region.SetWidth) then return end
  width = tonumber(width) or 0
  key = key or "_msufGFVisualWidth"
  if region[key] ~= width then
    region:SetWidth(width)
    region[key] = width
  end
end

--- Color writes can receive secret values on Midnight clients. Cache normal
--- values, but pass secret values through and clear cache stamps.
local function SetColorTextureCached(tex, r, g, b, a)
  if not (tex and tex.SetColorTexture) then return end
  r, g, b, a = r or 1, g or 1, b or 1, a or 1
  if issecretvalue(r) == true or issecretvalue(g) == true
    or issecretvalue(b) == true or issecretvalue(a) == true then
    tex:SetColorTexture(r, g, b, a)
    tex._msufGFVisualTextureKind = nil
    tex._msufGFVisualTexture = nil
    tex._msufGFVisualColorR = nil
    tex._msufGFVisualColorG = nil
    tex._msufGFVisualColorB = nil
    tex._msufGFVisualColorA = nil
    return
  end
  if tex._msufGFVisualTextureKind ~= "color" or tex._msufGFVisualColorR ~= r
    or tex._msufGFVisualColorG ~= g or tex._msufGFVisualColorB ~= b
    or tex._msufGFVisualColorA ~= a then
    tex:SetColorTexture(r, g, b, a)
    tex._msufGFVisualTextureKind = "color"
    tex._msufGFVisualTexture = nil
    tex._msufGFVisualVertexR = nil
    tex._msufGFVisualVertexG = nil
    tex._msufGFVisualVertexB = nil
    tex._msufGFVisualVertexA = nil
    tex._msufGFVisualColorR = r
    tex._msufGFVisualColorG = g
    tex._msufGFVisualColorB = b
    tex._msufGFVisualColorA = a
  end
end

local function LayoutTargetEdge(parent, edge, key, size)
  if edge._msufGFEdgeLayoutParent == parent
    and edge._msufGFEdgeLayoutKey == key
    and edge._msufGFEdgeLayoutSize == size then
    return
  end
  edge:ClearAllPoints()
  if key == "top" then
    edge:SetPoint("TOPLEFT", parent, "TOPLEFT", -size, size)
    edge:SetPoint("TOPRIGHT", parent, "TOPRIGHT", size, size)
    SetHeightCached(edge, size, "_msufGFEdgeHeight")
  elseif key == "bottom" then
    edge:SetPoint("BOTTOMLEFT", parent, "BOTTOMLEFT", -size, -size)
    edge:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", size, -size)
    SetHeightCached(edge, size, "_msufGFEdgeHeight")
  elseif key == "left" then
    edge:SetPoint("TOPLEFT", parent, "TOPLEFT", -size, size)
    edge:SetPoint("BOTTOMLEFT", parent, "BOTTOMLEFT", -size, -size)
    SetWidthCached(edge, size, "_msufGFEdgeWidth")
  else
    edge:SetPoint("TOPRIGHT", parent, "TOPRIGHT", size, size)
    edge:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", size, -size)
    SetWidthCached(edge, size, "_msufGFEdgeWidth")
  end
  edge._msufGFEdgeLayoutParent = parent
  edge._msufGFEdgeLayoutKey = key
  edge._msufGFEdgeLayoutSize = size
end

local function HideEdges(edges)
  if not edges then return end
  for i = 1, #EDGE_KEYS do
    SetShown(edges[EDGE_KEYS[i]], false)
  end
end

--- Compare unit tokens by GUID when possible so target/focus indicators still
--- work across party/raid token aliases.
local function SameUnit(unit, otherUnit)
  if issecretvalue(unit) == true then
    return false
  end
  if type(unit) ~= "string" or unit == "" then
    return false
  end
  if unit == otherUnit then
    return true
  end
  local guid = UnitGUID(unit)
  local otherGuid = UnitGUID(otherUnit)
  if issecretvalue(guid) == true or issecretvalue(otherGuid) == true then
    return false
  end
  return guid ~= nil and guid == otherGuid
end

local function SetEdgesShown(edges, shown)
  if not edges then return end
  for i = 1, #EDGE_KEYS do
    SetShown(edges[EDGE_KEYS[i]], shown)
  end
end

local function PrepareUnitEdges(frame, enabled, edgesKey, shownKey, layer, size, r, g, b)
  if not (frame and enabled) then
    HideEdges(frame and frame[edgesKey])
    if frame then frame[shownKey] = false end
    return
  end
  local edges = frame[edgesKey] or {}
  frame[edgesKey] = edges
  for i = 1, #EDGE_KEYS do
    local key = EDGE_KEYS[i]
    local edge = edges[key]
    if not edge then
      edge = frame:CreateTexture(nil, "OVERLAY", nil, layer)
      edges[key] = edge
    end
    LayoutTargetEdge(frame, edge, key, size)
    SetColorTextureCached(edge, r, g, b, 1)
    SetShown(edge, false)
  end
  frame[shownKey] = false
end

local function UpdateUnitEdges(frame, cfg, enabled, unit, edgesKey, shownKey, showOverride)
  if not (frame and cfg) then
    if frame then frame[shownKey] = false end
    SetEdgesShown(frame and frame[edgesKey], false)
    return
  end
  local show = showOverride
  if show == nil then
    show = enabled and frame.unit and SameUnit(frame.unit, unit) or false
  else
    show = show == true and enabled
  end
  if frame[shownKey] == show then
    return
  end
  frame[shownKey] = show
  SetEdgesShown(frame[edgesKey], show)
end

local function PrepareTarget(frame, cfg)
  PrepareUnitEdges(frame, cfg and cfg.targetIndicator == true,
    "MSUFGFTargetEdges", "_msufGFTargetVisualShown", 7, 2,
    cfg and cfg.targetR or 1, cfg and cfg.targetG or 1, cfg and cfg.targetB or 1)
end

local function UpdateTarget(frame, cfg, showOverride)
  UpdateUnitEdges(frame, cfg, cfg and cfg.targetIndicator == true,
    "target", "MSUFGFTargetEdges", "_msufGFTargetVisualShown", showOverride)
end

local function PrepareFocus(frame, cfg)
  local size = max(1, floor((tonumber(cfg and cfg.focusSize) or 2) + 0.5))
  local offset = tonumber(cfg and cfg.focusOffset) or 0
  PrepareUnitEdges(frame, cfg and cfg.focusIndicator == true,
    "MSUFGFFocusEdges", "_msufGFFocusVisualShown", 6, size + offset,
    cfg and cfg.focusR or 0.5, cfg and cfg.focusG or 0.5, cfg and cfg.focusB or 1)
end

local function UpdateFocus(frame, cfg, showOverride)
  UpdateUnitEdges(frame, cfg, cfg and cfg.focusIndicator == true,
    "focus", "MSUFGFFocusEdges", "_msufGFFocusVisualShown", showOverride)
end

local function AuraVisualTarget(frame, onHealth)
  if onHealth ~= false then
    return frame and (frame.hpBar or frame.Health or frame.health) or frame
  end
  return frame
end

local function LayoutAuraVisual(tex, target, edge, size)
  if not (tex and target) then return end
  edge = edge or "FULL"
  if edge ~= "TOP" and edge ~= "BOTTOM" and edge ~= "LEFT" and edge ~= "RIGHT" then
    edge = "FULL"
  end
  size = max(1, floor((tonumber(size) or 3) + 0.5))
  if tex._msufGFVisualTarget == target
    and tex._msufGFVisualEdge == edge
    and tex._msufGFVisualSize == size then
    return
  end
  tex:ClearAllPoints()
  if edge == "FULL" then
    tex:SetAllPoints(target)
  elseif edge == "TOP" then
    tex:SetPoint("TOPLEFT", target, "TOPLEFT", 0, 0)
    tex:SetPoint("TOPRIGHT", target, "TOPRIGHT", 0, 0)
    SetHeightCached(tex, size, "_msufGFAuraVisualHeight")
  elseif edge == "BOTTOM" then
    tex:SetPoint("BOTTOMLEFT", target, "BOTTOMLEFT", 0, 0)
    tex:SetPoint("BOTTOMRIGHT", target, "BOTTOMRIGHT", 0, 0)
    SetHeightCached(tex, size, "_msufGFAuraVisualHeight")
  elseif edge == "LEFT" then
    tex:SetPoint("TOPLEFT", target, "TOPLEFT", 0, 0)
    tex:SetPoint("BOTTOMLEFT", target, "BOTTOMLEFT", 0, 0)
    SetWidthCached(tex, size, "_msufGFAuraVisualWidth")
  else
    tex:SetPoint("TOPRIGHT", target, "TOPRIGHT", 0, 0)
    tex:SetPoint("BOTTOMRIGHT", target, "BOTTOMRIGHT", 0, 0)
    SetWidthCached(tex, size, "_msufGFAuraVisualWidth")
  end
  tex._msufGFVisualTarget = target
  tex._msufGFVisualEdge = edge
  tex._msufGFVisualSize = size
end

local function EnsureAuraTexture(frame, key)
  local tex = frame and frame[key]
  if tex then return tex end
  if not frame then return nil end
  tex = frame:CreateTexture(nil, "OVERLAY", nil, 5)
  tex:SetColorTexture(1, 1, 1, 1)
  tex:Hide()
  frame[key] = tex
  return tex
end

local function UpdateDispelOverlay(frame, cfg)
  local tex = frame and frame.MSUFGFDispelOverlay
  if not (cfg and cfg.dispelOverlayEnabled == true and frame and frame._msufA3DispelOverlayActive == true) then
    SetShown(tex, false)
    return
  end
  tex = tex or EnsureAuraTexture(frame, "MSUFGFDispelOverlay")
  if not tex then return end
  local target = AuraVisualTarget(frame, cfg.dispelOverlayOnHealth)
  LayoutAuraVisual(tex, target, cfg.dispelOverlayStyle or "FULL", cfg.highlightThickness or 3)
  SetColorTextureCached(tex,
    frame._msufA3DispelOverlayR or frame._msufA3DispelR or 0.25,
    frame._msufA3DispelOverlayG or frame._msufA3DispelG or 0.75,
    frame._msufA3DispelOverlayB or frame._msufA3DispelB or 1,
    cfg.dispelOverlayAlpha or 0.35)
  SetShown(tex, true)
end

local function UpdateDebuffStripe(frame, cfg)
  local tex = frame and frame.MSUFGFDebuffStripe
  if not (cfg and cfg.debuffStripeEnabled == true and frame and frame._msufA3DebuffStripeActive == true) then
    SetShown(tex, false)
    return
  end
  tex = tex or EnsureAuraTexture(frame, "MSUFGFDebuffStripe")
  if not tex then return end
  local target = frame.hpBar or frame.Health or frame
  LayoutAuraVisual(tex, target, frame._msufA3DebuffStripeEdge or cfg.debuffStripeEdge or "BOTTOM",
    frame._msufA3DebuffStripeHeight or cfg.debuffStripeHeight or 3)
  SetColorTextureCached(tex,
    frame._msufA3DebuffStripeR or cfg.debuffStripeColorR or 0.8,
    frame._msufA3DebuffStripeG or cfg.debuffStripeColorG or 0.2,
    frame._msufA3DebuffStripeB or cfg.debuffStripeColorB or 0.2,
    frame._msufA3DebuffStripeA or cfg.debuffStripeAlpha or 0.6)
  SetShown(tex, true)
end

local function UpdateAuraVisuals(frame, cfg)
  UpdateDispelOverlay(frame, cfg)
  UpdateDebuffStripe(frame, cfg)
end

local function UpdateAuraDispelOverlayOnly(frame, cfg)
  UpdateDispelOverlay(frame, cfg)
end

local function UpdateAuraDebuffStripeOnly(frame, cfg)
  UpdateDebuffStripe(frame, cfg)
end

local indicatorDriver
local targetIndicatorCount = 0
local focusIndicatorCount = 0
local targetDriverRegistered
local focusDriverRegistered
local targetIndicatorCurrentGUID
local focusIndicatorCurrentGUID
local targetIndicatorFrames = {}
local targetIndicatorIndex = {}
local focusIndicatorFrames = {}
local focusIndicatorIndex = {}
local indicatorGUIDBuckets = {}

local function PlainUnitGUID(unit)
  if not IsUnitToken(unit) then
    return nil
  end
  local guid = UnitGUID(unit)
  if issecretvalue(guid) == true or type(guid) ~= "string" or guid == "" then
    return nil
  end
  return guid
end

local function IndicatorFrameTracked(frame)
  return frame and (frame._msufGFTargetIndicatorRegistered == true or frame._msufGFFocusIndicatorRegistered == true)
end

local function RemoveIndicatorGUIDFrame(frame, guid)
  if not (frame and guid) then return end
  local bucket = indicatorGUIDBuckets[guid]
  local index = bucket and bucket.index
  local i = index and index[frame]
  if not i then return end
  local list = bucket.frames
  local last = #list
  local tail = list[last]
  list[i] = tail
  list[last] = nil
  index[frame] = nil
  if tail and tail ~= frame then
    index[tail] = i
  end
  if last <= 1 then
    indicatorGUIDBuckets[guid] = nil
  end
end

local function AddIndicatorGUIDFrame(frame, guid)
  if not (frame and guid and IndicatorFrameTracked(frame)) then return end
  local bucket = indicatorGUIDBuckets[guid]
  if not bucket then
    bucket = { frames = {}, index = {} }
    indicatorGUIDBuckets[guid] = bucket
  elseif bucket.index[frame] then
    return
  end
  local list = bucket.frames
  local n = #list + 1
  list[n] = frame
  bucket.index[frame] = n
end

local function RefreshIndicatorUnitGUID(frame)
  if not frame then
    return nil
  end
  local oldGuid = frame._msufGFVisualUnitGUID
  local guid = PlainUnitGUID(frame.unit)
  if oldGuid == guid then
    if guid and IndicatorFrameTracked(frame) then
      AddIndicatorGUIDFrame(frame, guid)
    end
    return guid
  end
  if oldGuid then
    RemoveIndicatorGUIDFrame(frame, oldGuid)
  end
  frame._msufGFVisualUnitGUID = guid
  if guid then
    AddIndicatorGUIDFrame(frame, guid)
  end
  return guid
end

local function RunIndicatorList(list, update, showOverride, refresh)
  local live = GF and GF.frames
  local doRefresh = refresh ~= false
  for i = 1, #list do
    local frame = list[i]
    if frame and (not live or live[frame] == true) then
      if doRefresh then
        RefreshIndicatorUnitGUID(frame)
      end
      update(frame, frame._msufGFVisualRuntimeGroup, showOverride)
    end
  end
end

local function RunIndicatorBucket(guid, update, registrationKey, show)
  if not guid then return end
  local bucket = indicatorGUIDBuckets[guid]
  local list = bucket and bucket.frames
  if not list then return end
  local live = GF and GF.frames
  for i = 1, #list do
    local frame = list[i]
    if frame and frame[registrationKey] == true
      and frame._msufGFVisualUnitGUID == guid
      and (not live or live[frame] == true) then
      update(frame, frame._msufGFVisualRuntimeGroup, show)
    end
  end
end

local function RunIndicatorEvent(event)
  local list, update, watchedUnit, oldGuid, registrationKey
  if event == "PLAYER_TARGET_CHANGED" then
    list, update, watchedUnit, oldGuid, registrationKey = targetIndicatorFrames, UpdateTarget, "target", targetIndicatorCurrentGUID, "_msufGFTargetIndicatorRegistered"
  else
    list, update, watchedUnit, oldGuid, registrationKey = focusIndicatorFrames, UpdateFocus, "focus", focusIndicatorCurrentGUID, "_msufGFFocusIndicatorRegistered"
  end
  local newGuid = PlainUnitGUID(watchedUnit)
  if event == "PLAYER_TARGET_CHANGED" then
    targetIndicatorCurrentGUID = newGuid
  else
    focusIndicatorCurrentGUID = newGuid
  end
  if oldGuid ~= nil and oldGuid == newGuid then
    return
  end
  if oldGuid ~= nil or newGuid ~= nil then
    RunIndicatorBucket(oldGuid, update, registrationKey, false)
    RunIndicatorBucket(newGuid, update, registrationKey, true)
    return
  end
  RunIndicatorList(list, update, false, false)
end

local function IndicatorDriverOnEvent(_, event)
  RunIndicatorEvent(event)
end

local function EnsureIndicatorDriver()
  if indicatorDriver or not CreateFrame then
    return indicatorDriver
  end
  indicatorDriver = CreateFrame("Frame")
  indicatorDriver:SetScript("OnEvent", IndicatorDriverOnEvent)
  return indicatorDriver
end

--- Unitless target/focus driver keeps edge indicators current without forcing
--- every group frame to listen to PLAYER_TARGET_CHANGED/PLAYER_FOCUS_CHANGED.
local function RefreshIndicatorDriver()
  if not indicatorDriver and targetIndicatorCount <= 0 and focusIndicatorCount <= 0 then
    return
  end
  local driver = EnsureIndicatorDriver()
  if not driver then
    return
  end
  local wantTarget = targetIndicatorCount > 0
  if wantTarget ~= targetDriverRegistered then
    if wantTarget then
      targetIndicatorCurrentGUID = PlainUnitGUID("target")
      driver:RegisterEvent("PLAYER_TARGET_CHANGED")
    else
      driver:UnregisterEvent("PLAYER_TARGET_CHANGED")
      targetIndicatorCurrentGUID = nil
    end
    targetDriverRegistered = wantTarget
  end
  local wantFocus = focusIndicatorCount > 0
  if wantFocus ~= focusDriverRegistered then
    if wantFocus then
      focusIndicatorCurrentGUID = PlainUnitGUID("focus")
      driver:RegisterEvent("PLAYER_FOCUS_CHANGED")
    else
      driver:UnregisterEvent("PLAYER_FOCUS_CHANGED")
      focusIndicatorCurrentGUID = nil
    end
    focusDriverRegistered = wantFocus
  end
end

local function AddIndicatorFrame(list, index, frame)
  if not frame or index[frame] then
    return
  end
  local n = #list + 1
  list[n] = frame
  index[frame] = n
end

local function RemoveIndicatorFrame(list, index, frame)
  local i = frame and index[frame]
  if not i then
    return
  end
  local last = #list
  local tail = list[last]
  list[i] = tail
  list[last] = nil
  index[frame] = nil
  if tail and tail ~= frame then
    index[tail] = i
  end
end

local function SetIndicatorRegistration(frame, target, focus)
  if not frame then
    return
  end
  target = target == true
  focus = focus == true
  local hadTarget = frame._msufGFTargetIndicatorRegistered == true
  if hadTarget ~= target then
    targetIndicatorCount = targetIndicatorCount + (target and 1 or -1)
    if targetIndicatorCount < 0 then targetIndicatorCount = 0 end
    frame._msufGFTargetIndicatorRegistered = target or nil
    if target then
      RefreshIndicatorUnitGUID(frame)
      AddIndicatorFrame(targetIndicatorFrames, targetIndicatorIndex, frame)
    else
      RemoveIndicatorFrame(targetIndicatorFrames, targetIndicatorIndex, frame)
    end
  end
  local hadFocus = frame._msufGFFocusIndicatorRegistered == true
  if hadFocus ~= focus then
    focusIndicatorCount = focusIndicatorCount + (focus and 1 or -1)
    if focusIndicatorCount < 0 then focusIndicatorCount = 0 end
    frame._msufGFFocusIndicatorRegistered = focus or nil
    if focus then
      RefreshIndicatorUnitGUID(frame)
      AddIndicatorFrame(focusIndicatorFrames, focusIndicatorIndex, frame)
    else
      RemoveIndicatorFrame(focusIndicatorFrames, focusIndicatorIndex, frame)
    end
  end
  if target ~= true and focus ~= true then
    RemoveIndicatorGUIDFrame(frame, frame._msufGFVisualUnitGUID)
    frame._msufGFVisualUnitGUID = nil
  end
  RefreshIndicatorDriver()
end

local function PercentFromValues(hp, maxHP)
  if issecretvalue(hp) == true or hp == nil then
    return nil
  end
  if issecretvalue(maxHP) == true or maxHP == nil then
    return nil
  end
  if type(hp) ~= "number" then
    hp = tonumber(hp)
  end
  if type(maxHP) ~= "number" then
    maxHP = tonumber(maxHP)
  end
  if hp and maxHP and maxHP > 0 then
    return (hp / maxHP) * 100
  end
  return nil
end

local function PercentFromPlainValues(hp, maxHP)
  if type(hp) ~= "number" then
    hp = tonumber(hp)
  end
  if type(maxHP) ~= "number" then
    maxHP = tonumber(maxHP)
  end
  if hp and maxHP and maxHP > 0 then
    return (hp / maxHP) * 100
  end
  return nil
end

local function CachedHealthValues(frame)
  local unit = frame and frame.unit
  local bar = frame and (frame.hpBar or frame.Health)
  if not (IsUnitToken(unit) and bar) then
    return nil, nil
  end
  local hpUnit = bar._msufHealthValueUnit
  local maxUnit = bar._msufHealthMaxUnit
  local hp = hpUnit == unit and bar._msufHealthValue or nil
  local maxHP = maxUnit == unit and bar._msufHealthMax or nil
  return hp, maxHP
end

--- Health fade is a hotpath visual. Use seeded dispatch values when available
--- and throttle secret-value fallbacks to avoid expensive repeated reads.
local function UpdateHealthFade(frame, cfg, seedHP, seedMaxHP, event)
  if not frame.hpBar then return end
  local rangeAlpha = frame._msufGFRangeHealthAlpha or 1
  local rangeBool = frame._msufGFRangeHealthBool
  local rangeBoolSecret = frame._msufGFRangeHealthBoolSecret == true or issecretvalue(rangeBool) == true
  local rangeBoolKnown = rangeBoolSecret or rangeBool ~= nil
  local rangeBoolIn = frame._msufGFRangeHealthBoolIn or 1
  local rangeBoolOut = frame._msufGFRangeHealthBoolOut or 1
  local keyHP, keyMax = seedHP, seedMaxHP
  local seedHPSecret = issecretvalue(seedHP) == true
  local seedMaxSecret = issecretvalue(seedMaxHP) == true
  local keyHPSecret = seedHPSecret
  local keyMaxSecret = seedMaxSecret
  local keyCacheable = keyHP ~= nil
    and keyMax ~= nil
    and not keyHPSecret
    and not keyMaxSecret
  if not keyCacheable then
    keyHP, keyMax = CachedHealthValues(frame)
    keyHPSecret = issecretvalue(keyHP) == true
    keyMaxSecret = issecretvalue(keyMax) == true
    keyCacheable = keyHP ~= nil
      and keyMax ~= nil
      and not keyHPSecret
      and not keyMaxSecret
  end
  if not keyCacheable and event == "UNIT_HEALTH" and GetTime then
    local now = GetTime()
    local nextAt = frame._msufGFHealthFadeSecretNextAt
    if frame._msufGFHealthFadeSecretRangeAlpha == rangeAlpha
      and frame._msufGFHealthFadeSecretRangeBoolKnown == rangeBoolKnown
      and frame._msufGFHealthFadeSecretRangeBoolIn == rangeBoolIn
      and frame._msufGFHealthFadeSecretRangeBoolOut == rangeBoolOut
      and (frame._msufGFVisualHealthAlpha ~= nil or frame._msufGFVisualHealthBoolApplied == true)
      and nextAt
      and now < nextAt then
      return
    end
    frame._msufGFHealthFadeSecretNextAt = now + HEALTH_FADE_SECRET_THROTTLE
    frame._msufGFHealthFadeSecretRangeAlpha = rangeAlpha
    frame._msufGFHealthFadeSecretRangeBoolKnown = rangeBoolKnown
    frame._msufGFHealthFadeSecretRangeBoolIn = rangeBoolIn
    frame._msufGFHealthFadeSecretRangeBoolOut = rangeBoolOut
  end
  if keyCacheable
    and rangeBoolSecret ~= true
    and frame._msufGFHealthFadeSeedHP == keyHP
    and frame._msufGFHealthFadeSeedMax == keyMax
    and frame._msufGFHealthFadeRangeAlpha == rangeAlpha
    and frame._msufGFHealthFadeRangeBoolKnown == rangeBoolKnown
    and frame._msufGFHealthFadeRangeBool == rangeBool
    and frame._msufGFHealthFadeRangeBoolIn == rangeBoolIn
    and frame._msufGFHealthFadeRangeBoolOut == rangeBoolOut then
    return
  end

  local alpha = 1
  local unit = frame.unit
  if cfg.healthFadeEnabled == true and IsUnitToken(unit) then
    local pct = keyCacheable and PercentFromPlainValues(keyHP, keyMax) or PercentFromValues(keyHP, keyMax)
    if pct == nil and UnitHealthPercent then
      local raw = UnitHealthPercent(unit)
      if issecretvalue(raw) ~= true then
        pct = tonumber(raw)
      end
    end
    if pct == nil
      and not seedHPSecret and seedHP == nil
      and not seedMaxSecret and seedMaxHP == nil
      and UnitHealth and UnitHealthMax then
      local hp, maxHP = UnitHealth(unit), UnitHealthMax(unit)
      pct = PercentFromValues(hp, maxHP)
    end
    if pct and pct >= (cfg.runtimeHealthFadeThreshold or cfg.healthFadeThreshold or 95) then
      alpha = alpha * (cfg.runtimeHealthFadeAlpha or cfg.healthFadeAlpha or 0.45)
    end
  end
  if keyCacheable then
    frame._msufGFHealthFadeSeedHP = keyHP
    frame._msufGFHealthFadeSeedMax = keyMax
    frame._msufGFHealthFadeRangeAlpha = rangeAlpha
    if rangeBoolSecret ~= true then
      frame._msufGFHealthFadeRangeBoolKnown = rangeBoolKnown
      frame._msufGFHealthFadeRangeBool = rangeBool
      frame._msufGFHealthFadeRangeBoolIn = rangeBoolIn
      frame._msufGFHealthFadeRangeBoolOut = rangeBoolOut
    else
      frame._msufGFHealthFadeRangeBoolKnown = nil
      frame._msufGFHealthFadeRangeBool = nil
      frame._msufGFHealthFadeRangeBoolIn = nil
      frame._msufGFHealthFadeRangeBoolOut = nil
    end
    frame._msufGFHealthFadeSecretNextAt = nil
    frame._msufGFHealthFadeSecretRangeAlpha = nil
    frame._msufGFHealthFadeSecretRangeBoolKnown = nil
    frame._msufGFHealthFadeSecretRangeBoolIn = nil
    frame._msufGFHealthFadeSecretRangeBoolOut = nil
  else
    frame._msufGFHealthFadeSeedHP = nil
    frame._msufGFHealthFadeSeedMax = nil
    frame._msufGFHealthFadeRangeAlpha = nil
    frame._msufGFHealthFadeRangeBoolKnown = nil
    frame._msufGFHealthFadeRangeBool = nil
    frame._msufGFHealthFadeRangeBoolIn = nil
    frame._msufGFHealthFadeRangeBoolOut = nil
  end
  if rangeBoolKnown then
    if SetAlphaFromBoolean(frame.hpBar, rangeBool, alpha * rangeBoolIn, alpha * rangeBoolOut, "_msufGFVisualHealthAlpha") then
      frame._msufGFVisualHealthAlpha = nil
      frame._msufGFVisualHealthBoolApplied = true
      return
    end
    local safe = PlainBool(rangeBool)
    if safe ~= nil then
      alpha = alpha * (safe and rangeBoolIn or rangeBoolOut)
    end
  else
    alpha = alpha * rangeAlpha
  end
  if frame._msufGFVisualHealthAlpha == alpha then
    return
  end
  SetAlphaCached(frame.hpBar, alpha, "_msufGFVisualHealthAlpha")
  frame._msufGFVisualHealthAlpha = alpha
  frame._msufGFVisualHealthBoolApplied = nil
end

local BarTextCommon
local function BarHelper(name)
  BarTextCommon = BarTextCommon or MSUF.UFBarTextCommon
  return BarTextCommon and BarTextCommon[name]
end

local function ApplyDeadBgColor(region, texture, r, g, b, a)
  local fn = BarHelper("ApplyTextureColor")
  if fn then fn(region, texture, r, g, b, a) end
end

local function RestoreHealthBackground(frame)
  local element = UF.elements and UF.elements.Health
  local active = frame and frame._msufActiveElements
  local unit = frame and frame.unit
  if element and element.Update and active and active.Health == true and IsUnitToken(unit) then
    local bar = frame.hpBar
    if bar then
      bar._msufStatusR = nil
      if frame._msufUnitState then frame._msufUnitState.ready = nil end
    end
    element.Update(frame, "MSUF_GF_VISUALS", unit)
  end
  local fn = BarHelper("ApplyBackgrounds")
  if fn then fn(frame, true, false) end
end

local function ResolveGone(frame, cfg, unit, seedHP, event)
  if not IsUnitToken(unit) then
    return false
  end
  local healthEvent = event == "UNIT_HEALTH" or event == "UNIT_MAXHEALTH"
  local state = frame and frame._msufUnitState
  local stateReady = state and state.ready == true
    and state.unit == unit
  local stateFresh = stateReady
    and frame._msufDispatchActive == true
    and state.dispatchToken == frame._msufDispatchToken
  local checkOffline = cfg.deadBgOffline == true
    and not healthEvent
    and (event ~= "UNIT_FLAGS" or frame._msufGFDeadBgState == true)
  if checkOffline and UnitIsConnected then
    if stateFresh and state.connectedKnown == true then
      if state.connected == false then
        return true
      end
    else
      local connected, known = ReadConnectedCached(frame, unit)
      if known == true and connected == false then
        return true
      end
    end
  end
  local deadKnown = stateFresh and state.deadKnown == true
  local dead = deadKnown and state.dead == true or false
  if dead then
    return true
  end
  if issecretvalue(seedHP) ~= true and type(seedHP) == "number" then
    if seedHP == 0 then
      return true
    end
    if healthEvent and frame._msufGFDeadBgState == true then
      return true
    end
    if frame._msufGFDeadBgState == true and not deadKnown then
      local dg, known = ReadDeadCached(frame, unit)
      if known == true and dg == true then
        return true
      end
    end
    return false
  end
  if not deadKnown then
    local dg, known = ReadDeadCached(frame, unit)
    if known == true and dg == true then
      return true
    end
  end
  return false
end

local function UpdateDeadBg(frame, cfg, seedHP, event)
  local bg = frame.bg
  local unit = frame.unit
  if not (bg and IsUnitToken(unit)) then return end
  local cached = frame._msufGFDeadBgState
  local gone = ResolveGone(frame, cfg, unit, seedHP, event)
  local firstResolve = cached == nil
  if gone then
    if cached == true and frame._msufHealthBgDynamic ~= true
      and frame._msufPowerBgDynamic ~= true then
      return
    end
    frame._msufGFDeadBgState = true
    local r, g, b = cfg.deadBgR or 0.6, cfg.deadBgG or 0.05, cfg.deadBgB or 0.05
    local a = cfg.deadBgA or 0.9
    local texture = frame._msufGFVisualHealthBackgroundTexture
    if texture == false then
      texture = nil
    elseif texture == nil and frame.MSUFSpec and frame.MSUFSpec.health then
      texture = frame.MSUFSpec.health.backgroundTexture
    end
    ApplyDeadBgColor(bg, texture, r, g, b, a)
    if frame.hpBarBG and frame.hpBarBG ~= bg then
      ApplyDeadBgColor(frame.hpBarBG, texture, r, g, b, a)
    end
    return
  end
  if cached == gone then return end
  frame._msufGFDeadBgState = gone
  if not firstResolve then
    RestoreHealthBackground(frame)
  end
end

local HealthFadeActive

local function SpecNeedsGroupVisuals(spec)
  local cfg = spec and spec.group
  if not cfg then return false end
  return HealthFadeActive(cfg) == true
    or cfg.targetIndicator == true
    or cfg.focusIndicator == true
    or cfg.deadBgEnabled == true
    or cfg.dispelOverlayEnabled == true
    or cfg.debuffStripeEnabled == true
end

local UpdateBordersFromVisualState

HealthFadeActive = function(cfg)
  if not (cfg and cfg.healthFadeEnabled == true) then
    return false
  end
  local alpha = tonumber(cfg.runtimeHealthFadeAlpha or cfg.healthFadeAlpha)
  if alpha == nil then
    alpha = 0.45
  end
  return alpha < 1
end

local function RuntimeOnRangeAlpha(frame, cfg)
  UpdateHealthFade(frame, cfg, nil, nil, "MSUF_GF_RANGE_ALPHA")
  local active = frame and frame._msufActiveElements
  if active and active.Borders == true then
    UpdateBordersFromVisualState(frame)
  end
end

local function PrepareVisuals(frame, cfg)
  if not (frame and cfg) then return end
  PrepareTarget(frame, cfg)
  PrepareFocus(frame, cfg)
end

local function CompileVisualRuntime(spec)
  local cfg = spec and spec.group
  if cfg then
    local healthActive
    cfg.runtimeHealthFadeThreshold = tonumber(cfg.healthFadeThreshold) or 95
    cfg.runtimeHealthFadeAlpha = tonumber(cfg.healthFadeAlpha) or 0.45
    healthActive = HealthFadeActive(cfg)
    cfg.runtimeOnHealth = healthActive and UpdateHealthFade or nil
    cfg.runtimeOnTarget = cfg.targetIndicator == true and UpdateTarget or nil
    cfg.runtimeOnFocus = cfg.focusIndicator == true and UpdateFocus or nil
    cfg.runtimeOnDeadBg = cfg.deadBgEnabled == true and UpdateDeadBg or nil
    cfg.runtimeOnRangeAlpha = (healthActive == true or (cfg.rangeFadeEnabled == true and cfg.rangeFadeLayerMode == "health")) and RuntimeOnRangeAlpha or nil
    if cfg.dispelOverlayEnabled == true then
      cfg.runtimeOnAuraVisuals = cfg.debuffStripeEnabled == true and UpdateAuraVisuals or UpdateAuraDispelOverlayOnly
    elseif cfg.debuffStripeEnabled == true then
      cfg.runtimeOnAuraVisuals = UpdateAuraDebuffStripeOnly
    else
      cfg.runtimeOnAuraVisuals = nil
    end
  end
end

local GroupVisuals = {}

function GroupVisuals.IsEnabled(frame, spec)
  return spec and spec.scope == "group" and SpecNeedsGroupVisuals(spec) == true
end

function GroupVisuals.GetEvents(frame, spec)
  local cfg = spec and spec.group
  if not cfg then return EMPTY_EVENTS end
  local health = HealthFadeActive(cfg)
  local offline = cfg.deadBgEnabled == true and cfg.deadBgOffline == true
  local flags = cfg.deadBgEnabled == true
  if health and offline then return VISUAL_HEALTH_OFFLINE_EVENTS end
  if health and flags then return VISUAL_HEALTH_FLAGS_EVENTS end
  if health then return VISUAL_HEALTH_EVENTS end
  if flags and offline then return VISUAL_FLAGS_OFFLINE_EVENTS end
  if flags then return VISUAL_FLAGS_EVENTS end
  if offline then return VISUAL_OFFLINE_EVENTS end
  return EMPTY_EVENTS
end

function GroupVisuals.GetUnitlessEvents(frame, spec)
  return EMPTY_EVENTS
end

UpdateBordersFromVisualState = function(frame)
  local active = frame and frame._msufActiveElements
  local borders = active and active.Borders == true and UF.elements and UF.elements.Borders
  if borders and borders.Update then
    borders.Update(frame, "MSUF_GF_VISUALS", frame.unit)
  end
end

local function UpdateVisuals(frame, event, updateInfo, seedMaxHP)
  local cfg = frame._msufGFVisualRuntimeGroup
  if not cfg then
    local spec = frame.MSUFSpec
    cfg = spec and spec.group
  end
  if not cfg then return end
  if event == "MSUF_GF_UNIT_IDENTITY" then
    local guid = RefreshIndicatorUnitGUID(frame)
    local fn = cfg.runtimeOnTarget
    if fn then fn(frame, cfg, guid ~= nil and guid == targetIndicatorCurrentGUID) end
    fn = cfg.runtimeOnFocus
    if fn then fn(frame, cfg, guid ~= nil and guid == focusIndicatorCurrentGUID) end
    fn = cfg.runtimeOnHealth
    if fn then fn(frame, cfg, updateInfo, seedMaxHP, event) end
    fn = cfg.runtimeOnDeadBg
    if fn then fn(frame, cfg, nil, event) end
    fn = cfg.runtimeOnAuraVisuals
    if fn then fn(frame, cfg, event) end
    return
  elseif event == "MSUF_GF_VISUALS_APPLY" then
    RefreshIndicatorUnitGUID(frame)
  end

  if event == "PLAYER_TARGET_CHANGED" then
    local fn = cfg.runtimeOnTarget
    if fn then
      fn(frame, cfg, event)
    end
    return
  elseif event == "PLAYER_FOCUS_CHANGED" then
    local fn = cfg.runtimeOnFocus
    if fn then
      fn(frame, cfg, event)
    end
    return
  elseif event == "UNIT_HEALTH" or event == "UNIT_MAXHEALTH" then
    local fn = cfg.runtimeOnHealth
    if fn then
      fn(frame, cfg, updateInfo, seedMaxHP, event)
    end
    return
  elseif event == "UNIT_CONNECTION" or event == "UNIT_FLAGS" then
    local fn = cfg.runtimeOnDeadBg
    if fn then
      fn(frame, cfg, nil, event)
    end
    return
  elseif event == "MSUF_GF_RANGE_ALPHA" then
    local fn = cfg.runtimeOnRangeAlpha
    if fn then
      fn(frame, cfg, event)
    end
    return
  elseif event == "MSUF_A3_AURA_VISUAL" then
    local fn = cfg.runtimeOnAuraVisuals
    if fn then
      fn(frame, cfg, event)
    end
    return
  end

  local fn = cfg.runtimeOnTarget
  if fn then fn(frame, cfg, event) end
  fn = cfg.runtimeOnFocus
  if fn then fn(frame, cfg, event) end
  fn = cfg.runtimeOnHealth
  if fn then fn(frame, cfg, updateInfo, seedMaxHP, event) end
  fn = cfg.runtimeOnDeadBg
  if fn then fn(frame, cfg, nil, event) end
  fn = cfg.runtimeOnAuraVisuals
  if fn then fn(frame, cfg, event) end

  if event == "MSUF_GF_VISUALS_APPLY" then
    UpdateBordersFromVisualState(frame)
  end
end

function GroupVisuals.UpdateHealthValue(frame, event, unit, seedHP, seedMaxHP)
  local fn = frame and frame._msufGFVisualRuntimeHealth
  if fn then
    fn(frame, frame._msufGFVisualRuntimeGroup, seedHP, seedMaxHP, event)
  end
end

function GroupVisuals.UpdateGoneState(frame, event)
  local fn = frame and frame._msufGFVisualRuntimeGone
  if fn then
    fn(frame, frame._msufGFVisualRuntimeGroup, nil, event)
  end
end

function GroupVisuals.Apply(frame)
  if frame then
    frame._msufGFDeadBgState = nil
    frame._msufGFHealthFadeSeedHP = nil
    frame._msufGFHealthFadeSeedMax = nil
    frame._msufGFHealthFadeRangeAlpha = nil
    frame._msufGFHealthFadeRangeBoolKnown = nil
    frame._msufGFHealthFadeRangeBool = nil
    frame._msufGFHealthFadeRangeBoolIn = nil
    frame._msufGFHealthFadeRangeBoolOut = nil
    frame._msufGFHealthFadeSecretRangeBoolKnown = nil
    frame._msufGFHealthFadeSecretRangeBoolIn = nil
    frame._msufGFHealthFadeSecretRangeBoolOut = nil
    frame._msufGFVisualHealthBoolApplied = nil
  end
  CompileVisualRuntime(frame and frame.MSUFSpec)
  local cfg = frame and frame.MSUFSpec and frame.MSUFSpec.group
  if frame then
    local healthFn = cfg and cfg.runtimeOnHealth or nil
    local goneFn = cfg and cfg.runtimeOnDeadBg or nil
    frame._msufGFVisualRuntimeGroup = cfg
    frame._msufGFVisualRuntimeHealth = healthFn
    frame._msufGFVisualRuntimeGone = goneFn
    frame._msufGFVisualHealthBackgroundTexture = frame.MSUFSpec and frame.MSUFSpec.health and frame.MSUFSpec.health.backgroundTexture or false
    frame._msufUpdateGroupVisualsHealthValue = healthFn and GroupVisuals.UpdateHealthValue or nil
    frame._msufUpdateGroupVisualsGoneState = goneFn and GroupVisuals.UpdateGoneState or nil
  end
  SetIndicatorRegistration(frame, cfg and cfg.targetIndicator == true, cfg and cfg.focusIndicator == true)
  PrepareVisuals(frame, cfg)
  UpdateVisuals(frame, "MSUF_GF_VISUALS_APPLY")
end
function GroupVisuals.Update(frame, event, unit, updateInfo, seedMaxHP) UpdateVisuals(frame, event, updateInfo, seedMaxHP) end

function GroupVisuals.Disable(frame)
  if not frame then return end
  SetIndicatorRegistration(frame, false, false)
  HideEdges(frame.MSUFGFTargetEdges)
  HideEdges(frame.MSUFGFFocusEdges)
  SetShown(frame.MSUFGFDispelOverlay, false)
  SetShown(frame.MSUFGFDebuffStripe, false)
  frame._msufGFTargetVisualShown = false
  frame._msufGFFocusVisualShown = false
  if frame._msufGFDeadBgState == true then
    RestoreHealthBackground(frame)
  end
  frame._msufGFDeadBgState = nil
  frame._msufGFHealthFadeSeedHP = nil
  frame._msufGFHealthFadeSeedMax = nil
  frame._msufGFHealthFadeRangeAlpha = nil
  frame._msufGFHealthFadeRangeBoolKnown = nil
  frame._msufGFHealthFadeRangeBool = nil
  frame._msufGFHealthFadeRangeBoolIn = nil
  frame._msufGFHealthFadeRangeBoolOut = nil
  frame._msufGFHealthFadeSecretRangeBoolKnown = nil
  frame._msufGFHealthFadeSecretRangeBoolIn = nil
  frame._msufGFHealthFadeSecretRangeBoolOut = nil
  frame._msufGFVisualHealthBoolApplied = nil
  frame._msufGFVisualRuntimeGroup = nil
  frame._msufGFVisualRuntimeHealth = nil
  frame._msufGFVisualRuntimeGone = nil
  frame._msufGFVisualHealthBackgroundTexture = nil
  frame._msufUpdateGroupVisualsHealthValue = nil
  frame._msufUpdateGroupVisualsGoneState = nil
end

UF.RegisterElement("GroupVisuals", GroupVisuals)
