-- rOrbs: Party Frames - Roth_UI portrait style
-- Retail 120001 / Lua 5.1 / oUF embedded

local addon, ns = ...
local oUF = ns.oUF or oUF
local cfg   -- assigned in Init
local func = ns.func

---------------------------------------------------------------------
--  DIMENSIONS  (runtime values – overwritten in Init() from rOrbsDB)
---------------------------------------------------------------------
local PORT_SIZE = 95
local HP_H      = 18
local HP_W      = 90
local PWR_H     = 7
local PWR_W     = 76
local NAME_H    = 22
local NAME_SIZE = 16
local HP_FONT   = 9
local FRAME_GAP   = 40
local FRAME_GAP_H = 40   -- horizontal gap (overwritten from rOrbsDB)
local FRAME_GAP_V = 40   -- vertical gap   (overwritten from rOrbsDB)
local ART_TOP    = 35
local ART_BOTTOM = 28
local ART_SIDE   = 35
local NAME_W  = 120
local FRAME_W = PORT_SIZE
-- barGap between HP and power bar must fit frame height (else layout extends past hitbox)
local function partyBarGap()
    return (rOrbsDB and rOrbsDB.partyBarGap) or 2
end

local FRAME_H = NAME_H + PORT_SIZE + HP_H + PWR_H + partyBarGap()

-- Retail: StatusBars/ping template may SetMouseClickEnabled(true) even when EnableMouse(false) —
-- then only gaps between bars receive clicks (parent). Recursively force click-through on descendants.
local function partyForceClickThrough(uf)
    if not uf then return end
    -- SecureUnitButton (party header): EnableMouse / walk on descendants is protected in combat (ADDON_ACTION_BLOCKED).
    if InCombatLockdown() then
        return
    end
    local function walk(f, depth)
        if not f or depth > 40 then return end
        for _, ch in ipairs({ f:GetChildren() }) do
            if ch then
                if ch._rOrbsPartyAuraRoot then
                    -- Do not recurse: aura icon buttons keep mouse for tooltips.
                else
                    if ch.EnableMouse then
                        ch:EnableMouse(false)
                    end
                    if ch.SetMouseClickEnabled then
                        pcall(function() ch:SetMouseClickEnabled(false) end)
                    end
                    if ch.SetMouseMotionEnabled then
                        pcall(function() ch:SetMouseMotionEnabled(false) end)
                    end
                    walk(ch, depth + 1)
                end
            end
        end
    end
    walk(uf, 0)
    uf:EnableMouse(true)
    uf:RegisterForClicks("AnyDown")
end

---------------------------------------------------------------------
--  INIT
---------------------------------------------------------------------
local function initUnitParameters(self)
    self:EnableMouse(true)
    self:RegisterForClicks("AnyDown")
    self:SetAttribute("*type1", "target")
    self:SetAttribute("*type2", "togglemenu")
    self:SetScript("OnEnter", UnitFrame_OnEnter)
    self:SetScript("OnLeave", UnitFrame_OnLeave)
end

---------------------------------------------------------------------
--  CUSTOM TAGS
---------------------------------------------------------------------
do
    -- Full class-coloured name, no truncation
    if not oUF.Tags.Methods["rOrbs:partyfullname"] then
        oUF.Tags.Methods["rOrbs:partyfullname"] = function(unit)
            if not UnitExists(unit) then return "" end
            local name = GetUnitName(unit) or ""
            if UnitIsPlayer(unit) then
                local _, class = UnitClass(unit)
                local cc = class and RAID_CLASS_COLORS and RAID_CLASS_COLORS[class]
                if cc then
                    return ("|cff%02x%02x%02x%s|r"):format(
                        cc.r * 255, cc.g * 255, cc.b * 255, name)
                end
            end
            return name
        end
        oUF.Tags.Events["rOrbs:partyfullname"] = "UNIT_NAME_UPDATE UNIT_CONNECTION"
    end
    -- legacy compat
    if not oUF.Tags.Methods["rOrbs:partyname"] then
        oUF.Tags.Methods["rOrbs:partyname"] =
            oUF.Tags.Methods["rOrbs:partyfullname"]
        oUF.Tags.Events["rOrbs:partyname"] =
            oUF.Tags.Events["rOrbs:partyfullname"]
    end
end

---------------------------------------------------------------------
--  NAME STRING  (inside top NAME_H strip of the oUF frame)
---------------------------------------------------------------------
local function createNameString(self)
    local LSM = LibStub("LibSharedMedia-3.0")
    local fontName = (rOrbsDB and rOrbsDB.partyNameFont) or cfg.fonts.textFont
    local fontPath = LSM:Fetch("font", fontName) or "Fonts\\FRIZQT__.TTF"
    local fontSize = (rOrbsDB and rOrbsDB.partyNameFontSize) or NAME_SIZE

    local name = self:CreateFontString(nil, "OVERLAY")
    name:SetFont(fontPath, fontSize, "THINOUTLINE")
    -- centred above portrait, wider than portrait so long names fit
    name:SetPoint("BOTTOM", self.PortraitHolder, "TOP", 0, -10)
    name:SetSize(NAME_W, NAME_H)
    name:SetJustifyH("CENTER")
    name:SetJustifyV("MIDDLE")
    name:SetTextColor(1, 1, 1, 1)
    self.Name = name

    self:Tag(name, "[rOrbs:partyfullname]")
end

---------------------------------------------------------------------
--  PORTRAIT BLOCK  (below NAME strip)
---------------------------------------------------------------------
local function createPortraitBlock(self)
    local holder = CreateFrame("Frame", nil, self)
    holder:SetSize(PORT_SIZE, PORT_SIZE)
    holder:SetPoint("TOPLEFT", self, "TOPLEFT", 0, -NAME_H)
    -- Avoid SetFrameStrata("BACKGROUND"): hit-test order vs sibling bars can steal portrait clicks from SecureUnitButton.
    holder:EnableMouse(false)
    self.PortraitHolder = holder

    local back = holder:CreateTexture(nil, "BACKGROUND", nil, -8)
    back:SetAllPoints(holder)
    back:SetTexture(ns.mediapath .. "portrait_back")
    back:SetVertexColor(0.1, 0.1, 0.1, 0.9)
    self.PortraitBack = back

    local use3D = (rOrbsDB and rOrbsDB.partyPortrait3D == true)  -- default: 2D (PlayerModel blocks clicks)
    if use3D then
        local model = CreateFrame("PlayerModel", nil, holder)
        model:SetPoint("TOPLEFT",     holder, "TOPLEFT",      21, -21)
        model:SetPoint("BOTTOMRIGHT", holder, "BOTTOMRIGHT", -21,  21)
        model:EnableMouse(false)
        model:SetUnit(self.unit or "player")
        model:EnableMouse(false)
        self:HookScript("OnEvent", function(s, event)
            if (event == "UNIT_MODEL_CHANGED" or event == "PLAYER_ENTERING_WORLD") and model:IsShown() then
                model:SetUnit(s.unit or "player")
                model:EnableMouse(false)
            end
        end)
        self.Portrait = model
    else
        local portrait = holder:CreateTexture(nil, "BACKGROUND", nil, -7)
        portrait:SetPoint("TOPLEFT",     holder, "TOPLEFT",      21, -21)
        portrait:SetPoint("BOTTOMRIGHT", holder, "BOTTOMRIGHT", -21,  21)
        portrait:SetTexCoord(0.15, 0.85, 0.15, 0.85)
        self.Portrait = portrait
    end

    local border = holder:CreateTexture(nil, "BACKGROUND", nil, -6)
    border:SetAllPoints(holder)
    border:SetTexture(ns.mediapath .. "portrait_border")
    border:SetVertexColor(0.6, 0.5, 0.5)
    self.Border = border

    local gloss = holder:CreateTexture(nil, "BACKGROUND", nil, -5)
    gloss:SetAllPoints(holder)
    gloss:SetTexture(ns.mediapath .. "portrait_gloss")
    gloss:SetVertexColor(0.9, 0.95, 1, 0.6)
end

---------------------------------------------------------------------
--  LOWER PART CONTAINER
--  Holds HP bar, power bar, and artwork.
--  Controlled by rOrbsDB.partyLowerMode:
--    "show"      – always visible (default)
--    "hide"      – always hidden
--    "mouseover" – visible on mouseover only
--  Independent scale: rOrbsDB.partyLowerScale (default 1.0)
---------------------------------------------------------------------
local function createLowerPartContainer(self)
    local lowerScale = (rOrbsDB and rOrbsDB.partyLowerScale) or 1.0

    -- Sits directly under PortraitHolder.
    -- Own frame so SetScale affects only this block.
    local lp = CreateFrame("Frame", nil, self)
    lp:SetSize(HP_W, HP_H + PWR_H + 4)   -- rough initial size; content sets final size
    lp:SetPoint("TOP", self.PortraitHolder, "BOTTOM", 0, 0)
    lp:SetScale(lowerScale)
    lp:SetFrameStrata("BACKGROUND")
    lp:EnableMouse(false)
    self._lowerPart = lp

    -- Initial visibility
    local mode = (rOrbsDB and rOrbsDB.partyLowerMode) or "show"
    if mode == "hide" then
        lp:Hide()
    else
        lp:Show()
    end

    return lp
end

---------------------------------------------------------------------
--  HEALTH BAR  (fixed-size container, child of _lowerPart)
---------------------------------------------------------------------
local function createHealthBar(self)
    local hcfg = self.cfg.health
    local lp   = self._lowerPart

    local con = CreateFrame("Frame", nil, lp)
    con:SetSize(HP_W, HP_H)
    con:SetPoint("TOP", lp, "TOP", 0, 0)
    con:SetFrameStrata("BACKGROUND")
    con:EnableMouse(false)
    self._hpContainer = con

    local h = CreateFrame("StatusBar", nil, con)
    h:SetAllPoints(con)
    h:SetStatusBarTexture(hcfg.texture)
    h:SetFrameStrata("BACKGROUND")
    h:EnableMouse(false)

    h.bg = h:CreateTexture(nil, "BORDER", nil, -6)
    h.bg:SetAllPoints(h)
    h.bg:SetTexture(hcfg.texture)
    h.bg:SetVertexColor(0.1, 0.1, 0.1, 0.9)

    -- h.glow = h:CreateTexture(nil, "OVERLAY", nil, -5)
    -- h.glow:SetTexture(ns.mediapath .. "targettarget_hpglow")
    -- h.glow:SetAllPoints(h)
    -- h.glow:SetVertexColor(0, 0, 0, 0.5)

    h.highlight = h:CreateTexture(nil, "OVERLAY", nil, -4)
    h.highlight:SetTexture(ns.mediapath .. "targettarget_highlight")
    h.highlight:SetAllPoints(h)

    local LSM = LibStub("LibSharedMedia-3.0")
    local fontPath = LSM:Fetch("font", cfg.fonts.textFont) or "Fonts\\FRIZQT__.TTF"
    local hpFontSize = (rOrbsDB and rOrbsDB.partyHpFontSize) or HP_FONT
    h.hpText = h:CreateFontString(nil, "OVERLAY")
    h.hpText:SetFont(fontPath, hpFontSize, "THINOUTLINE")
    h.hpText:SetPoint("CENTER", h, "CENTER", 0, 0)
    h.hpText:SetTextColor(1, 1, 1, 1)
    h.hpText:SetJustifyH("CENTER")

    h:EnableMouse(false)
    self.Health                 = h
    self.Health.Smooth          = true
    self.Health.frequentUpdates = hcfg.frequentUpdates or false
end

---------------------------------------------------------------------
--  POWER BAR  (fixed-size container below health bar, child of _lowerPart)
---------------------------------------------------------------------
local function createPowerBar(self)
    local pcfg = self.cfg.power
    local lp   = self._lowerPart

    local barGap = (rOrbsDB and rOrbsDB.partyBarGap) or 2
    local con = CreateFrame("Frame", nil, lp)
    con:SetSize(PWR_W, PWR_H)
    con:SetPoint("TOP", self._hpContainer, "BOTTOM", 0, -barGap)
    con:SetFrameStrata("BACKGROUND")
    con:EnableMouse(false)
    self._pwrContainer = con

    local p = CreateFrame("StatusBar", nil, con)
    p:SetAllPoints(con)
    p:SetStatusBarTexture(pcfg.texture)
    p:SetFrameStrata("BACKGROUND")
    p:EnableMouse(false)

    p.bg = p:CreateTexture(nil, "BORDER", nil, -6)
    p.bg:SetAllPoints(p)
    p.bg:SetTexture(pcfg.texture)
    p.bg:SetVertexColor(0.1, 0.1, 0.1, 0.9)

    p:EnableMouse(false)
    self.Power                 = p
    self.Power.Smooth          = true
    self.Power.frequentUpdates = pcfg.frequentUpdates or false
end

---------------------------------------------------------------------
--  DECORATIVE ARTWORK
--  Own frame child of _lowerPart so it scales/shows with the lower block
--  but renders above the bars (higher frame level).
---------------------------------------------------------------------
local function createArtwork(self)
    local artW = (rOrbsDB and rOrbsDB.partyArtW) or 160
    local artH = (rOrbsDB and rOrbsDB.partyArtH) or 55
    local artX = (rOrbsDB and rOrbsDB.partyArtX) or 0
    local artY = (rOrbsDB and rOrbsDB.partyArtY) or 0
    local lp   = self._lowerPart

    -- Dedicated artwork frame: frame level above _hpContainer / _pwrContainer
    -- so the texture draws in front of the bars.
    local artFrame = CreateFrame("Frame", nil, lp)
    artFrame:SetSize(artW, artH)
    artFrame:SetPoint("CENTER", self._hpContainer, "CENTER", artX, artY)
    artFrame:SetFrameLevel(lp:GetFrameLevel() + 10)
    artFrame:EnableMouse(false)

    local art = artFrame:CreateTexture(nil, "ARTWORK", nil, 0)
    art:SetTexture(ns.mediapath .. "targettarget")
    art:SetAllPoints(artFrame)

    self._artFrame = artFrame
end


---------------------------------------------------------------------
--  HEALTH POST-UPDATE  (party-specific color + hp text)
---------------------------------------------------------------------
local function updatePartyHpText(hpText, unit)
    if not hpText or not unit then return end
    local fmt = (rOrbsDB and rOrbsDB.partyHpTextFormat) or "value"
    if fmt == "percent" then
        ns.RenderPercent(hpText, unit, true)
    else
        ns.RenderAbbreviated(hpText, unit, true)
        if not UnitExists(unit) or UnitIsDeadOrGhost(unit) then
            hpText:SetText(DEAD)
        elseif not UnitIsConnected(unit) then
            hpText:SetText(PLAYER_OFFLINE)
        end
    end
end

local function healthPostUpdate(bar, unit, cur, max)
    -- ── Bar Color ─────────────────────────────────────────────────
    local r, g, b
    if UnitIsTapDenied(unit) then
        r, g, b = 0.65, 0.65, 0.65
    elseif UnitIsDeadOrGhost(unit) or not UnitIsConnected(unit) then
        r, g, b = 0.35, 0.35, 0.35
    elseif (rOrbsDB and rOrbsDB.partyHpClassColor) then
        -- UnitClass() works for follower-dungeon NPCs too (not just players)
        local _, class = UnitClass(unit)
        local cc = RAID_CLASS_COLORS and class and RAID_CLASS_COLORS[class]
        if cc then r, g, b = cc.r, cc.g, cc.b
        else     r, g, b = 0.8, 0.1, 0.1 end
    else
        r, g, b = 0.8, 0.1, 0.1
    end
    bar:SetStatusBarColor(r, g, b, 1)
    if bar.bg then
        bar.bg:SetVertexColor(r * 0.18, g * 0.18, b * 0.18, 0.9)
    end

    -- ── HP Text ────────────────────────────────────────────────────
    if bar.hpText then
        local mode = (rOrbsDB and rOrbsDB.partyHpTextMode) or "never"
        if mode == "never" then
            bar.hpText:Hide()
        elseif mode == "always" then
            bar.hpText:Show()
            updatePartyHpText(bar.hpText, unit)
        elseif mode == "mouseover" then
            if bar._mouseOver then
                updatePartyHpText(bar.hpText, unit)
            else
                bar.hpText:Hide()
            end
        end
    end
    local hpC = bar:GetParent()
    local lp = hpC and hpC:GetParent()
    local uf = lp and lp:GetParent()
    if uf and uf.cfg and uf.cfg.style == "party" then
        partyForceClickThrough(uf)
    end
end

---------------------------------------------------------------------
--  HELPER: apply LowerPart visibility
--  mode: "show" | "hide" | "mouseover"
--  over: true/false (current mouseover state)
---------------------------------------------------------------------
local function applyLowerPartVisibility(frame, mode, over)
    if not frame._lowerPart then return end
    if mode == "hide" then
        frame._lowerPart:Hide()
    elseif mode == "mouseover" then
        if over then
            frame._lowerPart:Show()
        else
            frame._lowerPart:Hide()
        end
    else  -- "show" (default)
        frame._lowerPart:Show()
    end
end

---------------------------------------------------------------------
--  PARTY AURAS (buff / debuff rows under lower bar, max width = HP bar)
---------------------------------------------------------------------
local function normalizePartyAuraMode(m)
    if m == "off" or m == "on" or m == "mouseover" then return m end
    return "mouseover"
end

local function partyAuraRowVisible(mode, unitMouseOver)
    if mode == "off" then return false end
    if mode == "on" then return true end
    return unitMouseOver == true
end

local function rOrbsIsFakePartyFrame(uf)
    local n = uf and uf.GetName and uf:GetName()
    if type(n) ~= "string" then return false end
    return n:match("^rOrbsFakePartyFrame%d") ~= nil
end

local function applyPartyAuraVisibility(uf, unitMouseOver)
    if not uf or not uf._partyAuraRoot then return end
    if ns.partyAuraPreview and rOrbsIsFakePartyFrame(uf) then
        uf._partyAuraRoot:Show()
        if uf.Buffs then uf.Buffs:Show() end
        if uf.Debuffs then uf.Debuffs:Show() end
        return
    end
    local bm = normalizePartyAuraMode(rOrbsDB and rOrbsDB.partyBuffAurasMode)
    local dm = normalizePartyAuraMode(rOrbsDB and rOrbsDB.partyDebuffAurasMode)
    local bShow = partyAuraRowVisible(bm, unitMouseOver)
    local dShow = partyAuraRowVisible(dm, unitMouseOver)
    if not bShow and not dShow then
        uf._partyAuraRoot:Hide()
        return
    end
    uf._partyAuraRoot:Show()
    if uf.Buffs then
        if bShow then uf.Buffs:Show() else uf.Buffs:Hide() end
    end
    if uf.Debuffs then
        if dShow then uf.Debuffs:Show() else uf.Debuffs:Hide() end
    end
end

---------------------------------------------------------------------
--  PARTY STYLE
---------------------------------------------------------------------
local function createStyle(self)
    self.cfg       = cfg.units.party
    self.cfg.style = "party"
    self:SetSize(FRAME_W, FRAME_H)

    initUnitParameters(self)
    createPortraitBlock(self)          -- first: name anchors to portrait
    createNameString(self)
    createLowerPartContainer(self)     -- HP / power / artwork container
    createHealthBar(self)
    createPowerBar(self)
    createArtwork(self)

    self.Health.PostUpdate = healthPostUpdate
    do
        local origPowerPU = func.updatePower
        -- oUF: element:PostUpdate(unit, cur, min, max) — lib.updatePower(bar, unit, min, max) uses same arg slots as direct assignment
        self.Power.PostUpdate = function(bar, unit, cur, min, max)
            origPowerPU(bar, unit, cur, min)
            local pc = bar and bar:GetParent()
            local lp = pc and pc:GetParent()
            local uf = lp and lp:GetParent()
            if uf and uf.cfg and uf.cfg.style == "party" then
                partyForceClickThrough(uf)
            end
        end
    end

    -- Buffs + debuffs below HP/power block; width capped to HP bar; tooltips on icons.
    do
        local spacing = (rOrbsDB and rOrbsDB.partyAuraSpacing) or 3
        local buffSz = math.max(8, math.floor(
            tonumber(rOrbsDB and rOrbsDB.partyBuffIconSize)
            or tonumber(rOrbsDB and rOrbsDB.partyAuraSize)
            or 12))
        local mul = tonumber(rOrbsDB and rOrbsDB.partyDebuffSizeMul) or 1.25
        if mul < 1 then mul = 1 elseif mul > 2.5 then mul = 2.5 end
        local debuffSz = math.max(8, math.floor(buffSz * mul + 0.5))

        local function rowSlots(barW, iconSz, sp)
            if not barW or barW <= 0 or not iconSz or iconSz <= 0 then return 1 end
            return math.max(1, math.floor((barW + sp) / (iconSz + sp)))
        end
        local buffCols = rowSlots(HP_W, buffSz, spacing)
        local debuffCols = rowSlots(HP_W, debuffSz, spacing)

        local root = CreateFrame("Frame", nil, self)
        root._rOrbsPartyAuraRoot = true
        root:SetWidth(HP_W)
        root:SetPoint("TOP", self._lowerPart, "BOTTOM",
            (rOrbsDB and rOrbsDB.partyAuraHostOffX) or 0,
            (rOrbsDB and rOrbsDB.partyAuraHostOffY) or -2)
        root:SetFrameStrata("MEDIUM")
        root:SetFrameLevel((self:GetFrameLevel() or 0) + 3)
        root:EnableMouse(false)
        self._partyAuraRoot = root

        local buffs = CreateFrame("Frame", nil, root)
        buffs:SetPoint("TOPLEFT", root, "TOPLEFT",
            (rOrbsDB and rOrbsDB.partyAuraBuffOffX) or 0,
            (rOrbsDB and rOrbsDB.partyAuraBuffOffY) or 0)
        buffs:SetWidth(HP_W)
        buffs.size = buffSz
        buffs.num = buffCols
        buffs.maxCols = buffCols
        buffs.initialAnchor = "TOPLEFT"
        buffs.growthX = "RIGHT"
        buffs.growthY = "DOWN"
        buffs.spacing = spacing
        buffs.disableCooldownText = self.cfg.auras.disableCooldownText
        self.Buffs = buffs

        local rowGap = (rOrbsDB and rOrbsDB.partyAuraRowGap) or 4
        local debuffs = CreateFrame("Frame", nil, root)
        debuffs:SetPoint("TOPLEFT", buffs, "BOTTOMLEFT",
            (rOrbsDB and rOrbsDB.partyAuraDebuffOffX) or 0,
            (-rowGap) + ((rOrbsDB and rOrbsDB.partyAuraDebuffOffY) or 0))
        debuffs:SetWidth(HP_W)
        debuffs.size = debuffSz
        debuffs.num = debuffCols
        debuffs.maxCols = debuffCols
        debuffs.initialAnchor = "TOPLEFT"
        debuffs.growthX = "RIGHT"
        debuffs.growthY = "DOWN"
        debuffs.spacing = spacing
        debuffs.disableCooldownText = self.cfg.auras.disableCooldownText
        debuffs.showDebuffType = self.cfg.auras.showDebuffType
        debuffs.onlyShowPlayer = self.cfg.auras.onlyShowPlayerDebuffs
        debuffs.onlyDispellable = self.cfg.auras.onlyDispellable
        self.Debuffs = debuffs

        local function postCreateAuraButton(icons, button)
            if func.createAuraIcon then func.createAuraIcon(icons, button) end
            if button then
                if button.RegisterForClicks then button:RegisterForClicks() end
                if button.SetMouseClickEnabled then
                    pcall(function() button:SetMouseClickEnabled(false) end)
                end
            end
        end
        buffs.PostCreateButton = postCreateAuraButton
        debuffs.PostCreateButton = postCreateAuraButton

        applyPartyAuraVisibility(self, false)
    end

    -- Debuff highlight recolours portrait border
    self.DebuffHighlight       = self.Border
    self.DebuffHighlightAlpha  = 1
    self.DebuffHighlightFilter = false

    self.Range = {
        insideAlpha  = 1,
        outsideAlpha = self.cfg.alpha.notinrange,
    }

    -- Raid target icon (skull, cross, etc.)
    -- oUF drives RaidTargetIndicator/LeaderIndicator/GroupRoleIndicator and calls Show() each update.
    -- Do not Hide() the texture itself — wrap each icon in a frame we control.
    -- Wrapper stays hidden when disabled; oUF toggles the texture only, not parent visibility.
    do
        local show = rOrbsDB and (rOrbsDB.partyRaidIconShow == true or rOrbsDB.partyRaidIconShow == nil)
        local size = (rOrbsDB and rOrbsDB.partyRaidIconSize) or 18
        local offX = (rOrbsDB and rOrbsDB.partyRaidIconOffX) or 4
        local offY = (rOrbsDB and rOrbsDB.partyRaidIconOffY) or 4
        -- Wrapper frame we control
        local wrapper = CreateFrame("Frame", nil, self.PortraitHolder)
        wrapper:SetSize(size, size)
        wrapper:SetPoint("TOPRIGHT", self.PortraitHolder, "TOPRIGHT", offX, offY)
        -- no SetFrameStrata: not allowed in secure context (inherits from parent)
        local raidIcon = wrapper:CreateTexture(nil, "OVERLAY", nil, 5)
        raidIcon:SetAllPoints(wrapper)
        raidIcon:SetTexture("Interface\\TargetingFrame\\UI-RaidTargetingIcons")
        self.RaidTargetIndicator = raidIcon
        self._raidIconWrapper = wrapper
        if not show then wrapper:Hide() end
    end

    -- Ready check (no GUI toggle; stays on PortraitHolder)
    local readyCheck = self.PortraitHolder:CreateTexture(nil, "OVERLAY", nil, 6)
    readyCheck:SetSize(24, 24)
    readyCheck:SetPoint("CENTER", self.PortraitHolder, "CENTER", 0, 0)
    self.ReadyCheckIndicator = readyCheck

    -- Leader crown
    -- Override oUF's LeaderIndicator update so it never calls SetAtlas(),
    -- which would overwrite our leader.tga with the Blizzard crown atlas.
    -- We drive show/hide ourselves based on the same logic as oUF.
    do
        local show = rOrbsDB and (rOrbsDB.partyLeaderIconShow == true or rOrbsDB.partyLeaderIconShow == nil)
        local size = (rOrbsDB and rOrbsDB.partyLeaderIconSize) or 13
        local offX = (rOrbsDB and rOrbsDB.partyLeaderIconOffX) or -2
        local offY = (rOrbsDB and rOrbsDB.partyLeaderIconOffY) or -2
        local wrapper = CreateFrame("Frame", nil, self.PortraitHolder)
        wrapper:SetSize(size, size)
        wrapper:SetPoint("BOTTOMLEFT", self.PortraitHolder, "BOTTOMLEFT", offX, offY)
        local leaderIcon = wrapper:CreateTexture(nil, "OVERLAY", nil, 7)
        leaderIcon:SetAllPoints(wrapper)
        leaderIcon:SetTexture(ns.mediapath .. "leader")
        self.LeaderIndicator = leaderIcon
        self._leaderIconWrapper = wrapper
        wrapper:Hide()  -- start hidden; first UpdateAllElements sets correct state

        -- Override: prevent oUF from calling SetAtlas on our texture.
        -- oUF passes the parent frame as first arg: Override(frame, event)
        leaderIcon.Override = function(frame, event)
            if not show then return end  -- respect GUI toggle
            local unit = frame.unit
            if not unit then return end
            local function applyLeader()
                if not (frame and frame._leaderIconWrapper) then return end
                local u = frame.unit
                if not u then return end
                local isLeader
                if IsInInstance() then
                    isLeader = UnitIsGroupLeader(u)
                else
                    isLeader = UnitLeadsAnyGroup(u)
                end
                -- Closure: use wrapper from enclosing do-block
                if isLeader then wrapper:Show() else wrapper:Hide() end
            end
            if event == "PLAYER_ENTERING_WORLD" then
                -- Defer: IsInInstance() / group state unstable right after loading screen → wait 0.5s
                C_Timer.After(0.5, applyLeader)
            else
                applyLeader()
            end
        end
    end

    -- Role icon (tank / healer / DPS)
    do
        local show = rOrbsDB and (rOrbsDB.partyRoleIconShow == true or rOrbsDB.partyRoleIconShow == nil)
        local size = (rOrbsDB and rOrbsDB.partyRoleIconSize) or 12
        local offX = (rOrbsDB and rOrbsDB.partyRoleIconOffX) or 2
        local offY = (rOrbsDB and rOrbsDB.partyRoleIconOffY) or -2
        local wrapper = CreateFrame("Frame", nil, self.PortraitHolder)
        wrapper:SetSize(size, size)
        wrapper:SetPoint("BOTTOMRIGHT", self.PortraitHolder, "BOTTOMRIGHT", offX, offY)
        local roleIcon = wrapper:CreateTexture(nil, "OVERLAY", nil, 7)
        roleIcon:SetAllPoints(wrapper)
        roleIcon:SetTexture("Interface\\LFGFrame\\UI-LFG-ICON-ROLES")
        self.GroupRoleIndicator = roleIcon
        self._roleIconWrapper = wrapper
        if not show then wrapper:Hide() end
    end

    self:RegisterEvent("UNIT_THREAT_SITUATION_UPDATE", func.checkThreat)
    self:RegisterEvent("UNIT_AURA", func.checkColors)

    -- Ensure no child frames steal mouse events.
    self.PortraitHolder:EnableMouse(false)
    if self._hpContainer  then self._hpContainer:EnableMouse(false)  end
    if self._pwrContainer then self._pwrContainer:EnableMouse(false) end
    if self._lowerPart    then self._lowerPart:EnableMouse(false)    end
    if self.Portrait and self.Portrait.EnableMouse then
        self.Portrait:EnableMouse(false)
    end

    -- OnUpdate: HP text mouseover + LowerPart mouseover + periodic click pass-through (retail mouse API)
    local wasOver = false
    local stripAccum = 0
    self:SetScript("OnUpdate", function(s, elapsed)
        stripAccum = stripAccum + (elapsed or 0)
        if stripAccum >= 0.5 then
            stripAccum = 0
            partyForceClickThrough(s)
        end
        local hpMode    = (rOrbsDB and rOrbsDB.partyHpTextMode)  or "never"
        local lowerMode = (rOrbsDB and rOrbsDB.partyLowerMode)   or "show"

        local over = s:IsMouseOver()
        if over == wasOver then return end
        wasOver = over

        -- HP text
        if hpMode == "mouseover" then
            if s.Health then
                s.Health._mouseOver = over
                if over then
                    if s.Health.hpText then
                        s.Health.hpText:Show()
                        updatePartyHpText(s.Health.hpText, s.unit)
                    end
                else
                    if s.Health.hpText then s.Health.hpText:Hide() end
                end
            end
        end

        -- LowerPart mouseover
        if lowerMode == "mouseover" then
            applyLowerPartVisibility(s, "mouseover", over)
        end

        applyPartyAuraVisibility(s, over)
    end)

    partyForceClickThrough(self)
end

---------------------------------------------------------------------
--  Hide Blizzard party UI (only outside combat lockdown —
-- Hide/Unregister on CompactPartyFrame is protected in combat)
---------------------------------------------------------------------
local function runPartySuppressBlizzard()
    if InCombatLockdown() then return end
    for _, f in ipairs({ CompactPartyFrame, _G["PartyFrame"] }) do
        if f then
            f:UnregisterAllEvents()
            f:Hide()
            if not f._rOrbsSuppressed then
                f._rOrbsSuppressed = true
                f.Show = function() end
            end
        end
    end
    if _G["PartyFrame"] then
        UnregisterStateDriver(_G["PartyFrame"], "visibility")
    end
end

---------------------------------------------------------------------
--  SPAWN / INIT
---------------------------------------------------------------------
local respawnQueued = false
local spawnedHeader

local function Init()
    cfg = ns.cfg
    -- Load layout dimensions from rOrbsDB
    if rOrbsDB then
        PORT_SIZE   = rOrbsDB.partyPortraitSize or PORT_SIZE
        NAME_H      = rOrbsDB.partyNameHeight    or NAME_H
        HP_H        = rOrbsDB.partyHpHeight      or HP_H
        HP_W        = rOrbsDB.partyHpWidth       or HP_W
        PWR_H       = rOrbsDB.partyPwrHeight     or PWR_H
        PWR_W       = rOrbsDB.partyPwrWidth      or PWR_W
        FRAME_GAP_H = rOrbsDB.partyGapH          or FRAME_GAP_H
        FRAME_GAP_V = rOrbsDB.partyGapV          or FRAME_GAP_V
        FRAME_W     = PORT_SIZE
        FRAME_H     = NAME_H + PORT_SIZE + HP_H + PWR_H + partyBarGap()
    end
    if respawnQueued then return end
    respawnQueued = true

    C_Timer.After(0, function()
        respawnQueued = false
        if not cfg.units.party.show then return end

        local styleName = "rOrbs:party" .. GetTime()

        if spawnedHeader then
            UnregisterUnitWatch(spawnedHeader)
            UnregisterStateDriver(spawnedHeader, "visibility")
            spawnedHeader:UnregisterAllEvents()
            spawnedHeader:Hide()
            spawnedHeader:SetParent(nil)
            if spawnedHeader:GetName() then _G[spawnedHeader:GetName()] = nil end
            spawnedHeader = nil
        end

        oUF:RegisterStyle(styleName, createStyle)
        oUF:SetActiveStyle(styleName)

        local attr = cfg.units.party.attributes
        local pcfg = cfg.units.party
        local L = ns.L

        local dragFrameName = "rOrbsPartyDragFrame"
        local partyDragFrame = _G[dragFrameName]
            or CreateFrame("Frame", dragFrameName, UIParent)
        partyDragFrame:SetSize(FRAME_W, FRAME_H)
        partyDragFrame:SetPoint(pcfg.pos.a1, pcfg.pos.af, pcfg.pos.a2,
            pcfg.pos.x, pcfg.pos.y)
        partyDragFrame:SetMovable(true)
        partyDragFrame:SetUserPlaced(true)
        partyDragFrame:SetClampedToScreen(true)
        partyDragFrame:EnableMouse(false)
        partyDragFrame._mouseEnabled = false
        partyDragFrame:RegisterForDrag("LeftButton")
        if not partyDragFrame._rOrbsModTex then
            local tex = partyDragFrame:CreateTexture(nil, "OVERLAY", nil, 7)
            tex:SetAllPoints(partyDragFrame)
            tex:SetColorTexture(1, 1, 1)
            tex:SetAlpha(0)
            partyDragFrame._rOrbsModTex = tex
        end
        -- Parent of SecureUnitButton frames: EnableMouse in OnUpdate is protected in combat
        -- (ADDON_ACTION_BLOCKED). Only outside lockdown; sync with shift after combat.
        if not partyDragFrame._rOrbsRegenSync then
            partyDragFrame._rOrbsRegenSync = CreateFrame("Frame", nil, partyDragFrame)
            partyDragFrame._rOrbsRegenSync:RegisterEvent("PLAYER_REGEN_ENABLED")
            partyDragFrame._rOrbsRegenSync:SetScript("OnEvent", function()
                local df = _G["rOrbsPartyDragFrame"]
                if not df or InCombatLockdown() then return end
                local need = IsShiftKeyDown() and not (rOrbsDB and rOrbsDB.orbDragLocked == true)
                if need ~= df._mouseEnabled then
                    df._mouseEnabled = need
                    df:EnableMouse(need)
                end
            end)
        end
        partyDragFrame:SetScript("OnUpdate", function(self)
            local needsMouse = IsShiftKeyDown() and not (rOrbsDB and rOrbsDB.orbDragLocked == true)
            if not InCombatLockdown() then
                if needsMouse ~= self._mouseEnabled then
                    self._mouseEnabled = needsMouse
                    self:EnableMouse(needsMouse)
                end
            end
            local tex = self._rOrbsModTex
            -- Position lock: no green/red overlay, no drag hint
            if rOrbsDB and rOrbsDB.orbDragLocked == true then
                if tex then tex:Hide() end
                return
            end
            local user = cfg.framesUserplaced ~= false
            local locked = cfg.framesLocked == true
            if user and tex then
                local sh, al = IsShiftKeyDown(), IsAltKeyDown()
                if locked then
                    if al then
                        tex:SetVertexColor(0.95, 0.2, 0.2)
                        tex:SetAlpha(0.32)
                        tex:Show()
                    elseif sh then
                        tex:SetVertexColor(0, 1, 0)
                        tex:SetAlpha(0.22)
                        tex:Show()
                    else
                        tex:Hide()
                    end
                else
                    tex:Show()
                    if al then
                        tex:SetVertexColor(0.95, 0.2, 0.2)
                        tex:SetAlpha(0.26)
                    else
                        tex:SetVertexColor(0, 1, 0)
                        tex:SetAlpha(0.18)
                    end
                end
            elseif tex then
                tex:Hide()
            end
        end)
        partyDragFrame:SetScript("OnDragStart", function(self)
            if rOrbsDB and rOrbsDB.orbDragLocked == true then return end
            if IsShiftKeyDown() then
                self:StartMoving()
            end
        end)
        partyDragFrame:SetScript("OnDragStop", function(self)
            self:StopMovingOrSizing()
            local _, _, _, x, y = self:GetPoint(1)
            if rOrbsDB and rOrbsDB.snapToGrid then
                x = math.floor(x / 10 + 0.5) * 10
                y = math.floor(y / 10 + 0.5) * 10
                local p, rt, rp = self:GetPoint(1)
                if p and rt then
                    self:ClearAllPoints()
                    self:SetPoint(p, rt, rp, x, y)
                end
            end
            if rOrbsDB then
                rOrbsDB.partyPosX = math.floor(x + 0.5)
                rOrbsDB.partyPosY = math.floor(y + 0.5)
                if cfg and cfg.units and cfg.units.party then
                    cfg.units.party.pos.x = rOrbsDB.partyPosX
                    cfg.units.party.pos.y = rOrbsDB.partyPosY
                end
            end
        end)

        if not partyDragFrame._rOrbsPartyTooltip then
            partyDragFrame._rOrbsPartyTooltip = true
            partyDragFrame:SetScript("OnEnter", function(self)
                if rOrbsDB and rOrbsDB.orbDragLocked == true then return end
                GameTooltip:SetOwner(self, "ANCHOR_TOP")
                GameTooltip:AddLine(L["PARTY_DRAG_TITLE"], 0, 1, 0.5, 1, 1, 1)
                GameTooltip:AddLine(L["PARTY_DRAG_HINT"], 1, 1, 1, 1, 1, 1)
                GameTooltip:Show()
            end)
            partyDragFrame:SetScript("OnLeave", function()
                GameTooltip:Hide()
            end)
        end

        local isVertical = rOrbsDB and rOrbsDB.partyVertical == true

        local headerName = "rOrbsPartyHeader" .. GetTime()
        local party = oUF:SpawnHeader(
            headerName, nil,
            "showPlayer", attr.showPlayer,
            "showSolo",   attr.showSolo,
            "showParty",  attr.showParty,
            "showRaid",   attr.showRaid,
            "point",      isVertical and "TOP"  or "LEFT",
            "xOffset",    isVertical and 0               or FRAME_GAP_H,
            "yOffset",    isVertical and -FRAME_GAP_V    or 0,
            "oUF-initialConfigFunction",
                (([[
                    self:SetWidth(%d)
                    self:SetHeight(%d)
                    self:SetScale(%f)
                ]]):format(FRAME_W, FRAME_H, pcfg.scale))
        )

        RegisterAttributeDriver(party, "state-visibility", attr.visibility)
        party:SetPoint("TOPLEFT", partyDragFrame, 0, 0)
        spawnedHeader = party
        ns.partyHeader = party

        if ns.RegisterBlizzardEditModeHost then
            ns.RegisterBlizzardEditModeHost(partyDragFrame, "party", { kind = "party" })
        end

        -- Drag parent must not eat clicks; hard reset after respawn
        partyDragFrame._mouseEnabled = false
        if not InCombatLockdown() then
            partyDragFrame:EnableMouse(false)
        end

        -- Pre-spawn 4 fake unit frames
        if not ns.fakePartyFrames then ns.fakePartyFrames = {} end
        for i = 1, 4 do
            local fname = "rOrbsFakePartyFrame" .. i
            local existing = _G[fname]
            if existing then
                existing:ClearAllPoints()
                if isVertical then
                    existing:SetPoint("TOPLEFT", partyDragFrame,
                        0, -((FRAME_H + FRAME_GAP_V) * (i - 1)))
                else
                    existing:SetPoint("TOPLEFT", partyDragFrame,
                        (FRAME_W + FRAME_GAP_H) * (i - 1), 0)
                end
                UnregisterUnitWatch(existing)
                existing:Hide()  -- ensure existing frames stay hidden
                -- OnShow blocker on existing frames too
                existing:SetScript("OnShow", function(self)
                    if not ns.fakePartyVisible then
                        UnregisterUnitWatch(self)
                        self:Hide()
                    end
                end)
                ns.fakePartyFrames[i] = existing
            else
                oUF:SetActiveStyle(styleName)
                local f = oUF:Spawn("player", fname)
                f:SetParent(partyDragFrame)
                f:ClearAllPoints()
                if isVertical then
                    f:SetPoint("TOPLEFT", partyDragFrame,
                        0, -((FRAME_H + FRAME_GAP_V) * (i - 1)))
                else
                    f:SetPoint("TOPLEFT", partyDragFrame,
                        (FRAME_W + FRAME_GAP_H) * (i - 1), 0)
                end
                UnregisterUnitWatch(f)
                -- OnShow blocker: stop oUF/RegisterUnitWatch from re-showing the frame
                -- after PLAYER_ENTERING_WORLD. ns.fakePartyVisible gates preview (GUI mode).
                f:SetScript("OnShow", function(self)
                    if not ns.fakePartyVisible then
                        UnregisterUnitWatch(self)
                        self:Hide()
                    end
                end)
                f:Hide()
                ns.fakePartyFrames[i] = f
            end
        end

        if attr.hideInArena then
            local f = CreateFrame("Frame")
            f:RegisterEvent("ARENA_PREP_OPPONENT_SPECIALIZATIONS")
            f:RegisterEvent("PLAYER_ENTERING_WORLD")
            f:SetScript("OnEvent", function()
                if not spawnedHeader then return end
                if IsActiveBattlefieldArena() then
                    RegisterStateDriver(spawnedHeader, "visibility", "hide")
                elseif cfg.units.party.show then
                    RegisterStateDriver(spawnedHeader, "visibility", attr.visibility)
                end
            end)
        end

        for _, child in ipairs({ spawnedHeader:GetChildren() }) do
            if child.UpdateAllElements then child:UpdateAllElements("ForceUpdate") end
        end

        C_Timer.After(0, function()
            if ns.ClickCast and ns.ClickCast.ApplyAll then ns.ClickCast.ApplyAll() end
        end)

        -- After PLAYER_ENTERING_WORLD (dungeon teleport / loading screen),
        -- oUF's RegisterUnitWatch re-evaluates all "player" unit frames and
        -- may Show() the fake preview frames. Re-hide them with a small delay
        -- so our Hide() runs AFTER oUF's own PLAYER_ENTERING_WORLD handlers.
        -- Register listener once (not on every init/respawn)
        if not ns._fakePartyHideListener then
            ns._fakePartyHideListener = CreateFrame("Frame")
            ns._fakePartyHideListener:RegisterEvent("PLAYER_ENTERING_WORLD")
            ns._fakePartyHideListener:SetScript("OnEvent", function()
                -- Hide immediately
                local function hideAll()
                    if ns.partyAuraPreview then return end
                    if ns.fakePartyFrames then
                        for _, fk in ipairs(ns.fakePartyFrames) do
                            if fk then
                                UnregisterUnitWatch(fk)
                                fk:Hide()
                            end
                        end
                    end
                end
                hideAll()
                -- Again after oUF PEW + RegisterUnitWatch callbacks
                C_Timer.After(0.1, hideAll)
                C_Timer.After(0.5, hideAll)
                C_Timer.After(1.5, hideAll)
            end)
        end

        runPartySuppressBlizzard()
        local sup = CreateFrame("Frame")
        sup:RegisterEvent("PLAYER_ENTERING_WORLD")
        sup:RegisterEvent("GROUP_ROSTER_UPDATE")
        sup:SetScript("OnEvent", function()
            runPartySuppressBlizzard()
        end)

        -- Re-sync auras after roster/instance changes (follower dungeons, NPC party members).
        if not ns._partyAuraRosterBump then
            ns._partyAuraRosterBump = CreateFrame("Frame")
            ns._partyAuraRosterBump:RegisterEvent("GROUP_ROSTER_UPDATE")
            ns._partyAuraRosterBump:RegisterEvent("PLAYER_ENTERING_WORLD")
            ns._partyAuraRosterBump:SetScript("OnEvent", function()
                C_Timer.After(0.15, function()
                    if not ns.partyHeader then return end
                    for _, ch in ipairs({ ns.partyHeader:GetChildren() }) do
                        if ch.UpdateAllElements and ch.cfg and ch.cfg.style == "party" then
                            ch:UpdateAllElements("ForceUpdate")
                        end
                    end
                end)
            end)
        end

        if not ns._rOrbsPartyBlizzRegen then
            ns._rOrbsPartyBlizzRegen = CreateFrame("Frame")
            ns._rOrbsPartyBlizzRegen:RegisterEvent("PLAYER_REGEN_ENABLED")
            ns._rOrbsPartyBlizzRegen:SetScript("OnEvent", runPartySuppressBlizzard)
        end

        local upd = CreateFrame("Frame")
        upd:SetScript("OnUpdate", function()
            if InCombatLockdown() then return end
            if CompactPartyFrame and CompactPartyFrame:IsShown() then
                CompactPartyFrame:Hide()
            end
            if _G["PartyFrame"] and _G["PartyFrame"]:IsShown() then
                _G["PartyFrame"]:Hide()
            end
        end)
    end)
end

-- Expose Init so the GUI layout checkboxes can trigger a full respawn
ns.RespawnParty = Init

-- Live apply LowerPart scale/mode to all active party frames without respawn
function ns.ApplyPartyLowerScale(scale)
    if not ns.partyHeader then return end
    for _, child in ipairs({ ns.partyHeader:GetChildren() }) do
        if child._lowerPart then
            child._lowerPart:SetScale(scale)
        end
    end
    if ns.fakePartyFrames then
        for _, f in ipairs(ns.fakePartyFrames) do
            if f._lowerPart then
                f._lowerPart:SetScale(scale)
            end
        end
    end
end

function ns.ApplyPartyLowerMode(mode)
    if not ns.partyHeader then return end
    for _, child in ipairs({ ns.partyHeader:GetChildren() }) do
        applyLowerPartVisibility(child, mode, false)
    end
    if ns.fakePartyFrames then
        for _, f in ipairs(ns.fakePartyFrames) do
            applyLowerPartVisibility(f, mode, false)
        end
    end
end

-- Live apply HP text font size to all active party frames
function ns.ApplyPartyHpFontSize(size)
    local function applyToFrame(f)
        if f.Health and f.Health.hpText then
            local font, _, flags = f.Health.hpText:GetFont()
            f.Health.hpText:SetFont(font or "Fonts\\FRIZQT__.TTF", size, flags or "THINOUTLINE")
        end
    end
    if ns.partyHeader then
        for _, child in ipairs({ ns.partyHeader:GetChildren() }) do
            applyToFrame(child)
        end
    end
    if ns.fakePartyFrames then
        for _, f in ipairs(ns.fakePartyFrames) do
            applyToFrame(f)
        end
    end
end

-- After all oUF EnableElement runs: click flags on descendants (incl. SetMouseClickEnabled)
do
    oUF:RegisterInitCallback(function(frame)
        if not frame.cfg or frame.cfg.style ~= "party" then return end
        partyForceClickThrough(frame)
    end)
end

-- Full party respawn from DB (used by Blizzard HUD edit mode live sliders)
function ns.RefreshPartyFramesFromDB()
    respawnQueued = false
    Init()
end

function ns.ApplyPartyAuraVisibilityAll()
    local function one(f)
        if f and f.cfg and f.cfg.style == "party" and f._partyAuraRoot then
            applyPartyAuraVisibility(f, f:IsMouseOver())
        end
    end
    if ns.partyHeader then
        for _, ch in ipairs({ ns.partyHeader:GetChildren() }) do
            one(ch)
        end
    end
    if ns.fakePartyFrames then
        for _, fk in ipairs(ns.fakePartyFrames) do
            one(fk)
        end
    end
end

-- GUI: show fake party frames (unit player) to align aura rows; ends on GUI close or button again.
function ns.ApplyPartyAuraPreview(enable)
    local want = enable and true or false
    local function run()
        if InCombatLockdown() then
            C_Timer.After(0.1, run)
            return
        end
        if want then
            ns.partyAuraPreview = true
            ns.fakePartyVisible = true
            if ns.fakePartyFrames then
                for _, f in ipairs(ns.fakePartyFrames) do
                    if f then
                        RegisterUnitWatch(f)
                        f:Show()
                        if f.UpdateAllElements then
                            f:UpdateAllElements("ForceUpdate")
                        end
                        applyPartyAuraVisibility(f, true)
                    end
                end
            end
        else
            ns.partyAuraPreview = false
            ns.fakePartyVisible = false
            if ns.fakePartyFrames then
                for _, f in ipairs(ns.fakePartyFrames) do
                    if f then
                        UnregisterUnitWatch(f)
                        f:Hide()
                    end
                end
            end
            if ns.ApplyPartyAuraVisibilityAll then
                ns.ApplyPartyAuraVisibilityAll()
            end
        end
    end
    run()
end

ns.RegisterInit(Init)
