-- 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 zwischen HP- und Power-Bar muss in der Frame-Höhe stecken (sonst ragt Layout unten aus der 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 können SetMouseClickEnabled(true) setzen, obwohl EnableMouse(false) ist —
-- dann fangen nur „Lücken“ zwischen Bars den Klick (Parent). Rekursiv alle Nachfahren durchklickbar machen.
local function partyForceClickThrough(uf)
    if not uf 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.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
    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)
    -- Kein SetFrameStrata("BACKGROUND"): sonst kann die Hit-Test-Reihenfolge zu Geschwistern (Bars) passen,
    -- sodass Klicks im Portrait nicht beim SecureUnitButton ankommen.
    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 blockiert Klicks)
    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
--  Enthält HP-Bar, Power-Bar und Artwork.
--  Kann über rOrbsDB.partyLowerMode gesteuert werden:
--    "show"      – immer sichtbar  (default)
--    "hide"      – immer versteckt
--    "mouseover" – nur bei Mouseover sichtbar
--  Unabhängiger Scale: rOrbsDB.partyLowerScale (default 1.0)
---------------------------------------------------------------------
local function createLowerPartContainer(self)
    local lowerScale = (rOrbsDB and rOrbsDB.partyLowerScale) or 1.0

    -- Der Container sitzt direkt unter dem PortraitHolder.
    -- Er bekommt einen eigenen Frame, damit SetScale nur ihn betrifft.
    local lp = CreateFrame("Frame", nil, self)
    lp:SetSize(HP_W, HP_H + PWR_H + 4)   -- grobe Initialgröße, Inhalte setzen echte Größe
    lp:SetPoint("TOP", self.PortraitHolder, "BOTTOM", 0, 0)
    lp:SetScale(lowerScale)
    lp:SetFrameStrata("BACKGROUND")
    lp:EnableMouse(false)
    self._lowerPart = lp

    -- Initiale Sichtbarkeit
    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, Kind von _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, Kind von _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
--  Liegt auf einem eigenen Frame-Child von _lowerPart, damit es
--  zusammen mit dem unteren Teil skaliert/ein-ausgeblendet wird,
--  aber ÜBER den Bar-Frames gerendert wird (höherer FrameLevel).
---------------------------------------------------------------------
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

    -- Eigener Frame für das Artwork: FrameLevel über _hpContainer und _pwrContainer,
    -- damit die Textur visuell vor den Bars liegt.
    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("DC")
        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: LowerPart Sichtbarkeit anwenden
--  mode: "show" | "hide" | "mouseover"
--  over: true/false (aktueller 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 STYLE
---------------------------------------------------------------------
local function createStyle(self)
    self.cfg       = cfg.units.party
    self.cfg.style = "party"
    self:SetSize(FRAME_W, FRAME_H)

    initUnitParameters(self)
    createPortraitBlock(self)          -- muss zuerst kommen (Name ancert daran)
    createNameString(self)
    createLowerPartContainer(self)     -- Container für HP/PWR/Artwork
    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) nutzt dieselben Slot-Positionen wie bei direkter Zuweisung
        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

    -- Auras below power bar
    if self.cfg.auras.show then
        local aSize = self.cfg.auras.size or 12
        local aNum  = self.cfg.auras.number or 4
        local f = CreateFrame("Frame", nil, self)
        f.disableCooldownText = self.cfg.auras.disableCooldownText
        f.size           = aSize
        f.num            = aNum
        f:SetSize((aSize + 3) * aNum, aSize + 4)
        f:SetPoint("TOPLEFT", self._pwrContainer, "BOTTOMLEFT", 0, -2)
        f.initialAnchor  = "TOPLEFT"
        f["growth-x"]    = "RIGHT"
        f["growth-y"]    = "DOWN"
        f.spacing        = 3
        f.showDebuffType = self.cfg.auras.showDebuffType
        f.onlyShowPlayer = self.cfg.auras.onlyShowPlayerDebuffs
        f.onlyDispellable = self.cfg.auras.onlyDispellable
        -- Klicks sollen den SecureUnitButton treffen (Target), nicht einzelne Aura-Buttons
        f.disableMouse = true
        self.Debuffs = f
        local basePost = func.createAuraIcon
        self.Debuffs.PostCreateIcon = function(icons, button)
            if basePost then basePost(icons, button) end
            if button then
                button:EnableMouse(false)
                if button.countFrame and button.countFrame.EnableMouse then
                    button.countFrame:EnableMouse(false)
                end
            end
        end
    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 (Marker-Icon: rotes X, Schädel, etc.)
    -- WICHTIG: oUF verwaltet RaidTargetIndicator/LeaderIndicator/GroupRoleIndicator
    -- selbst und ruft Show() bei jedem Update. Deshalb darf Hide() NICHT auf der
    -- Textur selbst aufgerufen werden – stattdessen packen wir jedes Icon in einen
    -- Wrapper-Frame. Der Wrapper wird dauerhaft Hide() gesetzt; oUF sieht nur die
    -- Textur und kann sie intern toggeln, das ändert aber nichts an der
    -- Sichtbarkeit des Parent-Frames.
    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: wird von uns kontrolliert
        local wrapper = CreateFrame("Frame", nil, self.PortraitHolder)
        wrapper:SetSize(size, size)
        wrapper:SetPoint("TOPRIGHT", self.PortraitHolder, "TOPRIGHT", offX, offY)
        -- kein SetFrameStrata: nicht erlaubt im Secure-Context (erbt vom 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 (kein GUI-Toggle, bleibt direkt auf 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 (Krone)
    -- 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()  -- immer versteckt starten; erstes UpdateAllElements setzt korrekten Zustand

        -- 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: wrapper aus dem do-Block direkt nutzen
                if isLeader then wrapper:Show() else wrapper:Hide() end
            end
            if event == "PLAYER_ENTERING_WORLD" then
                -- Defer: IsInInstance() und Group-State sind direkt nach dem
                -- Ladebildschirm noch nicht stabil → 0.5s warten.
                C_Timer.After(0.5, applyLeader)
            else
                applyLeader()
            end
        end
    end

    -- Role icon (Tank/Heiler/DD-Symbol)
    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)

    -- Sicherstellen dass keine Child-Frames Maus-Events blockieren.
    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 + periodisches Durchreichen der Klicks (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
    end)

    partyForceClickThrough(self)
end

---------------------------------------------------------------------
--  Blizzard Party UI ausblenden (nur außerhalb Combat Lockdown –
-- Hide/Unregister auf CompactPartyFrame sind im Kampf geschützt)
---------------------------------------------------------------------
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 von SecureUnitButton-Frames: EnableMouse in OnUpdate ist im Kampf geschützt
        -- (ADDON_ACTION_BLOCKED). Nur außerhalb Lockdown; nach Kampf mit Shift-Status syncen.
        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
            -- Positions-Sperre: kein gruenes/rotes Overlay, kein Hinweis auf Drag
            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 and L["PARTY_DRAG_TITLE"] or "rOrbs Party", 0, 1, 0.5, 1, 1, 1)
                GameTooltip:AddLine(L and L["PARTY_DRAG_HINT"] or "SHIFT + Drag to move", 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

        -- Drag-Parent darf keine Klicks schlucken; nach Respawn hart zurücksetzen
        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()  -- sicherstellen dass existierende Frames versteckt sind
                -- OnShow-Blocker auch auf existierende Frames setzen
                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: verhindert dass oUF/RegisterUnitWatch den
                -- Frame nach PLAYER_ENTERING_WORLD automatisch wieder einblendet.
                -- ns.fakePartyVisible steuert ob Vorschau erlaubt ist (GUI-Modus).
                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

        -- 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.
        -- Listener nur einmal registrieren (nicht bei jedem Init-/Respawn-Aufruf neu)
        if not ns._fakePartyHideListener then
            ns._fakePartyHideListener = CreateFrame("Frame")
            ns._fakePartyHideListener:RegisterEvent("PLAYER_ENTERING_WORLD")
            ns._fakePartyHideListener:SetScript("OnEvent", function()
                -- Sofort verstecken
                local function hideAll()
                    if ns.fakePartyFrames then
                        for _, fk in ipairs(ns.fakePartyFrames) do
                            if fk then
                                UnregisterUnitWatch(fk)
                                fk:Hide()
                            end
                        end
                    end
                end
                hideAll()
                -- Nach oUF PLAYER_ENTERING_WORLD + RegisterUnitWatch-Callbacks nochmal
                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)

        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-Funktion: LowerPart Scale/Mode auf alle aktiven Party-Frames anwenden
-- ohne Respawn (sofortige Wirkung im Spiel)
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-Funktion: HP-Text Fontgröße auf alle aktiven Party-Frames anwenden
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

-- Nach allen oUF-EnableElement-Läufen: Klick-Flags auf Nachfahren (inkl. SetMouseClickEnabled)
do
    oUF:RegisterInitCallback(function(frame)
        if not frame.cfg or frame.cfg.style ~= "party" then return end
        partyForceClickThrough(frame)
    end)
end

ns.RegisterInit(Init)
