-- ══════════════════════════════════════════════════════════════
-- MODULE: GuildCompetition
-- Gilden-Wettbewerbssystem mit Punkten, Kategorien & Leaderboard
-- Version 9.3.0
-- ══════════════════════════════════════════════════════════════

local addonName, addon = ...
local GDL = _G["GuildDeathLog"]
local GuildCompetition = {}

-- ── LOKALE API-REFERENZEN (Performance) ──────────────────────
local UnitClassification = UnitClassification
local UnitFactionGroup = UnitFactionGroup
local UnitLevel = UnitLevel
local UnitName = UnitName
local UnitCreatureType = UnitCreatureType
local GetInstanceInfo = GetInstanceInfo
local IsInInstance = IsInInstance
local IsInGuild = IsInGuild
local GetGuildInfo = GetGuildInfo
local GetNumGuildMembers = GetNumGuildMembers
local time = time
local pairs = pairs
local ipairs = ipairs
local math_floor = math.floor
local math_max = math.max
local string_format = string.format
local table_sort = table.sort
local table_insert = table.insert

-- Compat-Layer (TBC/Classic)
local addonTable = select(2, ...)
local Compat = addonTable._compat or {}
local CompatSendAddonMessage = Compat.SendAddonMessage or (C_ChatInfo and C_ChatInfo.SendAddonMessage) or SendAddonMessage
local CompatRegisterPrefix = Compat.RegisterAddonMessagePrefix or (C_ChatInfo and C_ChatInfo.RegisterAddonMessagePrefix) or RegisterAddonMessagePrefix

-- ── KONSTANTEN ───────────────────────────────────────────────
local COMP_PREFIX = "GDLComp"        -- Addon-Channel fuer Cross-Guild
local SYNC_INTERVAL = 300            -- Alle 5 Min eigene Daten broadcasten
local SCORE_VERSION = 1              -- Protokoll-Version fuer Kompatibilitaet

-- Punktewerte nach Gegner-Klassifizierung
local POINTS = {
    -- Klassifizierung (UnitClassification)
    normal      = 1,   -- Normaler Mob
    trivial     = 0,   -- Grauer Mob (zu niedrig)
    elite       = 5,   -- Elite
    rare        = 10,  -- Selten
    rareelite   = 15,  -- Selten + Elite
    worldboss   = 50,  -- Weltboss

    -- Sonderkategorien
    dungeon_boss = 25, -- Dungeon-Boss (Instanz-Kill, kein Elite-Flag noetig)

    -- Strafen
    member_death = -500, -- Tod eines Gildenmitglieds
}

-- Gegnertyp-Kategorien fuer die Anzeige
local CATEGORY_NAMES = {
    trivial     = "Graue Gegner",
    normal      = "Normale Gegner",
    elite       = "Elite-Gegner",
    rare        = "Seltene Gegner",
    rareelite   = "Seltene Elites",
    dungeon_boss = "Dungeon-Bosse",
    worldboss   = "Weltbosse",
}

local CATEGORY_ORDER = {
    "worldboss", "dungeon_boss", "rareelite", "rare", "elite", "normal", "trivial"
}

local CATEGORY_COLORS = {
    trivial      = {0.5, 0.5, 0.5},   -- Grau
    normal       = {0.2, 0.8, 0.2},   -- Gruen
    elite        = {1.0, 0.84, 0.0},  -- Gold
    rare         = {0.0, 0.44, 0.87}, -- Blau
    rareelite    = {0.64, 0.21, 0.93},-- Lila
    dungeon_boss = {1.0, 0.5, 0.0},   -- Orange
    worldboss    = {1.0, 0.0, 0.0},   -- Rot
}

-- Bekannte Dungeon-Bosse (Classic)
-- Wird als Fallback genutzt wenn UnitClassification nicht reicht
local KNOWN_DUNGEON_BOSSES = {
    -- Deadmines
    ["Rhahk'Zor"] = true, ["Sneed"] = true, ["Gilnid"] = true,
    ["Mr. Smite"] = true, ["Edwin VanCleef"] = true, ["Cookie"] = true,
    -- Wailing Caverns
    ["Lady Anacondra"] = true, ["Lord Cobrahn"] = true, ["Kresh"] = true,
    ["Lord Pythas"] = true, ["Skum"] = true, ["Lord Serpentis"] = true,
    ["Verdan the Everliving"] = true, ["Mutanus the Devourer"] = true,
    -- Shadowfang Keep
    ["Rethilgore"] = true, ["Baron Silverlaine"] = true,
    ["Commander Springvale"] = true, ["Odo the Blindwatcher"] = true,
    ["Fenrus the Devourer"] = true, ["Wolf Master Nandos"] = true,
    ["Archmage Arugal"] = true,
    -- Stockade
    ["Targorr the Dread"] = true, ["Kam Deepfury"] = true,
    ["Hamhock"] = true, ["Bazil Thredd"] = true, ["Dextren Ward"] = true,
    -- Gnomeregan
    ["Grubbis"] = true, ["Viscous Fallout"] = true,
    ["Electrocutioner 6000"] = true, ["Crowd Pummeler 9-60"] = true,
    ["Mekgineer Thermaplugg"] = true,
    -- Scarlet Monastery
    ["Interrogator Vishas"] = true, ["Bloodmage Thalnos"] = true,
    ["Houndmaster Loksey"] = true, ["Arcanist Doan"] = true,
    ["Herod"] = true, ["High Inquisitor Fairbanks"] = true,
    ["Scarlet Commander Mograine"] = true, ["High Inquisitor Whitemane"] = true,
    -- Razorfen Kraul / Downs
    ["Agathelos the Raging"] = true, ["Charlga Razorflank"] = true,
    ["Amnennar the Coldbringer"] = true, ["Glutton"] = true,
    -- Uldaman
    ["Ironaya"] = true, ["Archaedas"] = true,
    -- Zul'Farrak
    ["Antu'sul"] = true, ["Witch Doctor Zum'rah"] = true,
    ["Hydromancer Velratha"] = true, ["Chief Ukorz Sandscalp"] = true,
    ["Gahz'rilla"] = true,
    -- Maraudon
    ["Noxxion"] = true, ["Razorlash"] = true, ["Tinkerer Gizlock"] = true,
    ["Lord Vyletongue"] = true, ["Celebras the Cursed"] = true,
    ["Landslide"] = true, ["Rotgrip"] = true, ["Princess Theradras"] = true,
    -- Sunken Temple
    ["Avatar of Hakkar"] = true, ["Shade of Eranikus"] = true,
    ["Jammal'an the Prophet"] = true,
    -- Blackrock Depths
    ["High Interrogator Gerstahn"] = true, ["Lord Roccor"] = true,
    ["Houndmaster Grebmar"] = true, ["Bael'Gar"] = true,
    ["General Angerforge"] = true, ["Golem Lord Argelmach"] = true,
    ["Ribbly Screwspigot"] = true, ["Ambassador Flamelash"] = true,
    ["Emperor Dagran Thaurissan"] = true, ["Princess Moira Bronzebeard"] = true,
    -- LBRS / UBRS
    ["Overlord Wyrmthalak"] = true, ["Quartermaster Zigris"] = true,
    ["General Drakkisath"] = true, ["Warchief Rend Blackhand"] = true,
    ["The Beast"] = true,
    -- Stratholme
    ["Timmy the Cruel"] = true, ["Cannon Master Willey"] = true,
    ["Balnazzar"] = true, ["Baron Rivendare"] = true,
    ["Ramstein the Gorger"] = true,
    -- Scholomance
    ["Darkmaster Gandling"] = true, ["Kirtonos the Herald"] = true,
    ["Jandice Barov"] = true, ["Rattlegore"] = true,
    ["Ras Frostwhisper"] = true,
    -- Dire Maul
    ["King Gordok"] = true, ["Alzzin the Wildshaper"] = true,
    ["Immol'thar"] = true, ["Prince Tortheldrin"] = true,
    ["Guard Mol'dar"] = true, ["Illyanna Ravenoak"] = true,
    ["Tendris Warpwood"] = true,
    -- Weltbosse
    ["Azuregos"] = true, ["Lord Kazzak"] = true,
    ["Emeriss"] = true, ["Lethon"] = true, ["Taerar"] = true, ["Ysondre"] = true,
}

-- ── INTERNES STATE ───────────────────────────────────────────
local playerData = {}  -- { [playerName] = { kills = { [category] = count }, points = 0 } }
local guildScore = 0
local guildDeathPenalty = 0
local leaderboard = {} -- { [guildName] = { points = N, members = N, lastUpdate = time } }
local syncTimer = nil
local saveTimer = nil  -- Dirty-Save Timer (speichert verzoegert)
local initialized = false
local dirtyFlag = true -- Dirty-Flag fuer Neuberechnung
local saveDirty = false -- Dirty-Flag fuer Speicherung

-- ══════════════════════════════════════════════════════════════
-- PUNKT-BERECHNUNG
-- ══════════════════════════════════════════════════════════════

--- Klassifiziert einen Gegner und gibt Kategorie + Punkte zurueck
function GuildCompetition:ClassifyEnemy(unitID, mobName)
    -- Bekannter Boss?
    if mobName and KNOWN_DUNGEON_BOSSES[mobName] then
        return "dungeon_boss", POINTS.dungeon_boss
    end

    -- UnitClassification nutzen
    local classification = unitID and UnitClassification(unitID) or "normal"

    -- In Instanz + Elite = Dungeon Boss
    local inInstance = IsInInstance()
    if inInstance and (classification == "elite" or classification == "rareelite") then
        -- Level-Check: Wenn der Mob mindestens Instanz-Level hat, ist's ein Boss
        local mobLevel = unitID and UnitLevel(unitID) or 0
        if mobLevel >= 15 then -- Mindest-Level fuer relevante Dungeon-Bosse
            return "dungeon_boss", POINTS.dungeon_boss
        end
    end

    -- Weltboss
    if classification == "worldboss" then
        return "worldboss", POINTS.worldboss
    end

    -- Rare Elite
    if classification == "rareelite" then
        return "rareelite", POINTS.rareelite
    end

    -- Rare
    if classification == "rare" then
        return "rare", POINTS.rare
    end

    -- Elite (ausserhalb von Instanzen)
    if classification == "elite" then
        return "elite", POINTS.elite
    end

    -- Trivial (grauer Mob - zu niedrig)
    if classification == "trivial" or classification == "minus" then
        return "trivial", POINTS.trivial
    end

    -- Normal
    return "normal", POINTS.normal
end

-- ══════════════════════════════════════════════════════════════
-- DATEN-MANAGEMENT
-- ══════════════════════════════════════════════════════════════

function GuildCompetition:GetDB()
    if not GuildDeathLogDB then return nil end
    if not GuildDeathLogDB.guildCompetition then
        GuildDeathLogDB.guildCompetition = {
            playerKills = {},   -- LEGACY (flach, accountweit) - nur noch Migrationsquelle
            guildPoints = 0,
            deathPenalty = 0,
            leaderboard = {},
            lastReset = time(),
        }
    end
    -- Migration: pro Fraktion+Gilde getrennte Buckets (seit 9.9.x)
    GuildDeathLogDB.guildCompetition.scopes = GuildDeathLogDB.guildCompetition.scopes or {}
    return GuildDeathLogDB.guildCompetition
end

-- Aktuelle Fraktion ("Alliance"/"Horde"/"Neutral")
local function CurrentFaction()
    local f = UnitFactionGroup and UnitFactionGroup("player")
    return f or "Neutral"
end

-- Scope-Schluessel = FRAKTION::GILDE. Trennt Wettbewerbsdaten sauber zwischen
-- Chars verschiedener Fraktionen/Gilden (auch bei gleichnamigen Gilden je Seite).
-- Ohne Gilde -> nil (ohne Gilde gibt es keinen Gildenwettbewerb).
function GuildCompetition:GetScopeKey()
    local g = GDL.currentGuildName or GDL.guildName or (GetGuildInfo and GetGuildInfo("player"))
    if not g or g == "" then return nil end
    return CurrentFaction() .. "::" .. g
end

-- Liefert den Daten-Bucket fuer den AKTUELLEN Scope (Fraktion+Gilde).
function GuildCompetition:GetScopeDB()
    local root = self:GetDB()
    if not root then return nil end
    local key = self:GetScopeKey()
    if not key then return nil end
    if not root.scopes[key] then
        root.scopes[key] = {
            playerKills  = {},
            playerPoints = {},
            guildPoints  = 0,
            deathPenalty = 0,
            leaderboard  = {},
        }
    end
    return root.scopes[key], key
end

-- Wechselt der Char (Gilde/Fraktion) waehrend der Session, ohne Reload:
-- alten Scope sichern, neuen laden. Verhindert Cross-Scope-Vermischung.
function GuildCompetition:_CheckScope()
    if not initialized then return end
    if self:GetScopeKey() ~= self._loadedScopeKey then
        self:SaveState()
        self:LoadState()
    end
end

function GuildCompetition:SaveState()
    -- WICHTIG: in den Bucket schreiben, AUS DEM geladen wurde (_loadedScopeKey),
    -- nicht in den aktuellen — sonst landen bei Gildenwechsel die alten Spieler
    -- im neuen Scope.
    local root = self:GetDB()
    if not root then return end
    local key = self._loadedScopeKey
    if not key then return end
    root.scopes[key] = root.scopes[key] or {}
    local scope = root.scopes[key]

    scope.playerKills  = {}
    scope.playerPoints = {}
    for name, data in pairs(playerData) do
        scope.playerKills[name]  = data.kills
        scope.playerPoints[name] = data.points
    end
    scope.guildPoints  = guildScore
    scope.deathPenalty = guildDeathPenalty
    scope.leaderboard  = leaderboard
end

function GuildCompetition:LoadState()
    local myName = GDL.playerName

    -- State immer leeren, BEVOR ein neuer Scope geladen wird (Gildenwechsel!)
    playerData = {}
    guildScore = 0
    guildDeathPenalty = 0
    leaderboard = {}

    local scope, key = self:GetScopeDB()
    self._loadedScopeKey = key
    if not scope then
        -- Keine Gilde -> kein Wettbewerb. Nichts laden.
        dirtyFlag = true
        return
    end

    -- ── Einmalige Migration der alten FLACHEN Daten ──────────────────────────
    -- Nur den AKTUELLEN Char uebernehmen (einziger sicher zuordenbarer Eintrag).
    -- Fremd-Fraktion-Twinks bleiben so garantiert draussen; Gildenkollegen
    -- befuellen ihren Scope ohnehin per Broadcast neu.
    local root = self:GetDB()
    if not scope._migrated then
        if root.playerKills and myName and root.playerKills[myName] and not (scope.playerKills or {})[myName] then
            scope.playerKills  = scope.playerKills  or {}
            scope.playerPoints = scope.playerPoints or {}
            scope.playerKills[myName]  = root.playerKills[myName]
            scope.playerPoints[myName] = (root.playerPoints and root.playerPoints[myName]) or 0
        end
        scope._migrated = true
    end

    for name, kills in pairs(scope.playerKills or {}) do
        -- Selbstheilung: vergiftetes _remote_total des EIGENEN Spielers
        -- entfernen (Altlast des Echo-Bugs). Eigene Kills sind lokal
        -- vollstaendig bekannt, der Pseudo-Wert hat hier nichts verloren.
        if name == myName then
            kills._remote_total = nil
        end

        -- Punkte: bevorzugt der gespeicherte Wert. Die alte Ableitung aus
        -- den Kill-Kategorien lieferte fuer Remote-Spieler 0 (deren kills
        -- enthalten nur _remote_total) -> nach jedem Relog standen Gilden-
        -- kollegen mit 0 Punkten da, bis ihr naechster Broadcast kam.
        local storedPts = scope.playerPoints and scope.playerPoints[name]
        local computedPts = 0
        for cat, count in pairs(kills) do
            computedPts = computedPts + (count * (POINTS[cat] or 0))
        end
        -- Eigener Spieler: lokale Kill-Kategorien sind autoritativ.
        local pts = (name == myName) and computedPts or (storedPts or computedPts)
        playerData[name] = { kills = kills, points = pts }
    end
    guildScore = scope.guildPoints or 0
    guildDeathPenalty = scope.deathPenalty or 0
    leaderboard = scope.leaderboard or {}
    dirtyFlag = true -- Score nach Heilung/Laden frisch berechnen
end

-- ══════════════════════════════════════════════════════════════
-- KILL-TRACKING (wird von KillStats aufgerufen)
-- ══════════════════════════════════════════════════════════════

--- Registriert einen Kill fuer den aktuellen Spieler
--- @param mobName string Name des getoeteten Mobs
--- @param unitID string|nil UnitID falls verfuegbar
--- @param killerName string|nil Name des Killers (Gildenmitglied)
function GuildCompetition:RegisterKill(mobName, unitID, killerName)
    if not initialized then return end
    if not IsInGuild() then return end
    self:_CheckScope()

    local player = killerName or GDL.playerName
    if not player then return end

    local category, points = self:ClassifyEnemy(unitID, mobName)

    -- Spielerdaten anlegen/aktualisieren
    if not playerData[player] then
        playerData[player] = { kills = {}, points = 0 }
    end

    local pd = playerData[player]
    pd.kills[category] = (pd.kills[category] or 0) + 1
    pd.points = pd.points + points

    -- Gildenscore aktualisieren
    guildScore = guildScore + points
    dirtyFlag = true
    saveDirty = true

    -- Verzoegert speichern (2s nach letztem Kill — batched)
    if not saveTimer then
        saveTimer = C_Timer.NewTimer(2, function()
            saveTimer = nil
            if saveDirty then
                self:SaveState()
                saveDirty = false
            end
        end)
    end
end

--- Registriert einen Gildenmitglied-Tod (Strafpunkte)
function GuildCompetition:RegisterDeath(playerName)
    if not initialized then return end
    if not IsInGuild() then return end
    self:_CheckScope()

    guildDeathPenalty = guildDeathPenalty + math.abs(POINTS.member_death)
    guildScore = guildScore + POINTS.member_death -- negativ!
    dirtyFlag = true
    self:SaveState()

    GDL:Print(string_format(
        "|cffFF4040Gildenwertung:|r Tod von %s kostet |cffFF0000%d Punkte|r!",
        playerName or "?", math.abs(POINTS.member_death)
    ))
end

-- ══════════════════════════════════════════════════════════════
-- SCORE-BERECHNUNG & ABFRAGEN
-- ══════════════════════════════════════════════════════════════

--- Berechnet den Gesamtscore der eigenen Gilde neu
function GuildCompetition:RecalculateScore()
    local total = 0
    for _, data in pairs(playerData) do
        total = total + data.points
    end
    total = total - guildDeathPenalty -- Todes-Strafen separat
    -- Nein, Strafen sind schon in guildScore drin via RegisterDeath
    -- Also einfach playerData summieren + death penalty
    guildScore = total
    dirtyFlag = false
    return guildScore
end

--- Gibt den aktuellen Gildenscore zurueck
function GuildCompetition:GetGuildScore()
    if dirtyFlag then
        self:RecalculateScore()
    end
    return math_max(0, guildScore) -- Nie unter 0 anzeigen
end

--- Gibt den Rang der eigenen Gilde im Leaderboard zurueck
function GuildCompetition:GetGuildRank()
    local myGuild = GDL.guildName or (GetGuildInfo("player"))
    if not myGuild then return 0, 0 end

    local sorted = self:GetSortedLeaderboard()
    for i, entry in ipairs(sorted) do
        if entry.name == myGuild then
            return i, #sorted
        end
    end
    return #sorted + 1, #sorted + 1
end

--- Gibt das sortierte Leaderboard zurueck (eigene Gilde inkludiert)
function GuildCompetition:GetSortedLeaderboard()
    local myGuild = GDL.guildName or (GetGuildInfo("player"))
    local list = {}

    -- Eigene Gilde eintragen
    if myGuild then
        table_insert(list, {
            name = myGuild,
            points = self:GetGuildScore(),
            members = GetNumGuildMembers() or 0,
            isOwn = true,
        })
    end

    -- Andere Gilden aus Leaderboard
    for guildName, data in pairs(leaderboard) do
        if guildName ~= myGuild then
            -- Nur Eintraege die nicht aelter als 7 Tage sind
            if data.lastUpdate and (time() - data.lastUpdate) < 604800 then
                table_insert(list, {
                    name = guildName,
                    points = data.points or 0,
                    members = data.members or 0,
                    isOwn = false,
                })
            end
        end
    end

    -- Sortieren: Hoechste Punkte zuerst
    table_sort(list, function(a, b) return a.points > b.points end)

    return list
end

--- Gibt die Spieler-Rangliste der eigenen Gilde zurueck
function GuildCompetition:GetPlayerRanking()
    local list = {}
    local Sync = GDL:GetModule("Sync")
    for name, data in pairs(playerData) do
        -- Anforderung: "Unsere Kaempfer" zeigt NUR Spieler, die das Addon nutzen
        -- (self oder bekannter Addon-User aus syncedUsers). Schuetzt auch gegen
        -- Altdaten von vor der Scope-Trennung.
        local hasAddon = (name == GDL.playerName)
            or (Sync and Sync.HasAddon and Sync:HasAddon(name))
        if hasAddon then
            local totalKills = 0
            for cat, count in pairs(data.kills) do
                if cat ~= "_remote_total" then
                    totalKills = totalKills + count
                end
            end
            -- Remote-Spieler: totalKills aus Sync nehmen falls hoeher
            if data.kills._remote_total and data.kills._remote_total > totalKills then
                totalKills = data.kills._remote_total
            end
            table_insert(list, {
                name = name,
                points = data.points,
                kills = data.kills,
                totalKills = totalKills,
            })
        end
    end

    table_sort(list, function(a, b) return a.points > b.points end)
    return list
end

--- Gibt Statistiken fuer einen bestimmten Spieler zurueck
function GuildCompetition:GetPlayerStats(playerName)
    local data = playerData[playerName]
    if not data then return nil end

    local stats = {
        name = playerName,
        points = data.points,
        kills = {},
        totalKills = 0,
    }

    for _, cat in ipairs(CATEGORY_ORDER) do
        local count = data.kills[cat] or 0
        if count > 0 then
            table_insert(stats.kills, {
                category = cat,
                name = GDL:T(CATEGORY_NAMES[cat]),
                count = count,
                points = count * (POINTS[cat] or 0),
                color = CATEGORY_COLORS[cat],
            })
            stats.totalKills = stats.totalKills + count
        end
    end

    return stats
end

-- ══════════════════════════════════════════════════════════════
-- CROSS-GUILD SYNC
-- ══════════════════════════════════════════════════════════════

--- Registriert den Cross-Guild Addon-Channel
function GuildCompetition:RegisterChannel()
    if not CompatRegisterPrefix then return end
    pcall(function()
        CompatRegisterPrefix(COMP_PREFIX)
    end)
end

--- Sendet den eigenen Gildenscore an alle im Channel
function GuildCompetition:BroadcastScore()
    if not IsInGuild() then return end
    if not CompatSendAddonMessage then return end
    self:_CheckScope()

    local myGuild = GDL.guildName or (GetGuildInfo("player"))
    if not myGuild then return end

    local score = self:GetGuildScore()
    local members = GetNumGuildMembers() or 0

    -- Typ G: Gilden-Gesamtscore (fuer Cross-Guild Leaderboard)
    -- Format: VER|G|GUILD|SCORE|MEMBERS
    local gMsg = string_format("%d|G|%s|%d|%d",
        SCORE_VERSION, myGuild, score, members)

    -- An eigene Gilde
    pcall(CompatSendAddonMessage, COMP_PREFIX, gMsg, "GUILD")

    -- An Instanz-Gruppe (Cross-Guild)
    if IsInInstance() then
        pcall(CompatSendAddonMessage, COMP_PREFIX, gMsg, "INSTANCE_CHAT")
    end

    -- An Yell-Range (Cross-Guild)
    pcall(CompatSendAddonMessage, COMP_PREFIX, gMsg, "YELL")

    -- Typ P: Eigene Spieler-Stats (nur an Gilde!)
    -- Format: VER|P|PLAYER|POINTS|TOTALKILLS
    local myName = GDL.playerName
    if myName and playerData[myName] then
        local pd = playerData[myName]
        local totalKills = 0
        -- _remote_total NIEMALS mitsummieren: das ist ein Sync-Pseudo-Wert.
        -- Vorher floss er in die Summe ein -> bei Eigen-Echo verdoppelte sich
        -- der Kill-Zaehler mit jedem Broadcast (978 -> 119.967).
        for cat, count in pairs(pd.kills) do
            if cat ~= "_remote_total" then
                totalKills = totalKills + count
            end
        end
        local pMsg = string_format("%d|P|%s|%d|%d",
            SCORE_VERSION, myName, pd.points, totalKills)
        pcall(CompatSendAddonMessage, COMP_PREFIX, pMsg, "GUILD")
    end
end

--- Verarbeitet eingehende Cross-Guild/Intra-Guild Nachrichten
function GuildCompetition:HandleAddonMessage(prefix, msg, channel, sender)
    if prefix ~= COMP_PREFIX then return end

    -- Eigene Nachrichten ignorieren. WICHTIG: CHAT_MSG_ADDON liefert den
    -- Sender als "Name-Realm", GDL.playerName ist aber der nackte Name —
    -- der alte Vergleich griff deshalb NIE und wir haben unser eigenes
    -- Echo verarbeitet (Ursache der explodierenden Kill-Zahlen).
    local shortSender = sender and strsplit("-", sender) or nil
    if shortSender == GDL.playerName then return end

    -- Parse Message-Typ
    local ver, msgType, rest = msg:match("^(%d+)|([GP])|(.*)")

    -- Fallback: Altes Format ohne Typ (v1 kompatibilitaet)
    if not ver then
        ver, rest = msg:match("^(%d+)|(.*)")
        if ver then msgType = "G" end
    end
    if not ver then return end

    ver = tonumber(ver)
    if ver ~= SCORE_VERSION then return end

    if msgType == "G" then
        -- Gilden-Score: guildName|score|members
        local guildName, score, members = rest:match("^(.+)|(-?%d+)|(%d+)$")
        if not guildName then return end
        score = tonumber(score)
        members = tonumber(members)
        if not score then return end

        -- Eigene Gilde: Score nicht uebernehmen (wir haben genauere lokale Daten)
        local myGuild = GDL.guildName or (GetGuildInfo("player"))
        if guildName == myGuild then return end

        -- Leaderboard aktualisieren
        if not leaderboard[guildName] then
            leaderboard[guildName] = {}
        end
        local entry = leaderboard[guildName]
        if not entry.points or score >= entry.points then
            entry.points = score
            entry.members = members or 0
            entry.lastUpdate = time()
        end
        saveDirty = true

    elseif msgType == "P" then
        -- Spieler-Stats gehoeren in die "Unsere Kaempfer"-Liste -> NUR vom
        -- GUILD-Channel akzeptieren (= garantiert eigene Gilde/Fraktion).
        -- Sonst koennten Cross-Guild-Nachrichten (YELL/INSTANCE) fremde Spieler
        -- einschleusen.
        if channel ~= "GUILD" then return end
        self:_CheckScope()
        -- Spieler-Stats (nur von Gildenmitgliedern): playerName|points|totalKills
        local pName, pPoints, pKills = rest:match("^(.+)|(-?%d+)|(%d+)$")
        if not pName then return end
        -- Daten ueber UNS SELBST nie von remote uebernehmen — lokale Daten
        -- sind autoritativ. (Zweite Verteidigungslinie gegen Echo/Alts.)
        if pName == GDL.playerName then return end
        pPoints = tonumber(pPoints)
        pKills = tonumber(pKills)
        if not pPoints then return end

        -- Spielerdaten aktualisieren (remote: nur Gesamtwerte, keine Kill-Kategorien)
        if not playerData[pName] then
            playerData[pName] = { kills = {}, points = 0 }
        end
        local pd = playerData[pName]
        -- Nur aktualisieren wenn remote hoeher (verhindert Rollback)
        if pPoints >= pd.points then
            pd.points = pPoints
            -- totalKills in einen Pseudo-Kategorie-Eintrag
            pd.kills._remote_total = pKills
        end
        dirtyFlag = true
        saveDirty = true
    end
end

--- Startet den periodischen Broadcast-Timer
function GuildCompetition:StartSyncTimer()
    if syncTimer then return end

    syncTimer = C_Timer.NewTicker(SYNC_INTERVAL, function()
        if IsInGuild() then
            self:BroadcastScore()
        end
    end)

    -- Erster Broadcast nach 30s (Login-Spam abwarten)
    C_Timer.After(30, function()
        if IsInGuild() then
            self:BroadcastScore()
        end
    end)
end

-- ══════════════════════════════════════════════════════════════
-- GETTER FUER UI (UIChapters braucht diese)
-- ══════════════════════════════════════════════════════════════

--- Gibt die Punkt-Konstanten zurueck
function GuildCompetition:GetPointValues()
    return POINTS
end

--- Gibt die Kategorie-Namen zurueck
function GuildCompetition:GetCategoryNames()
    return CATEGORY_NAMES
end

--- Gibt die Kategorie-Reihenfolge zurueck
function GuildCompetition:GetCategoryOrder()
    return CATEGORY_ORDER
end

--- Gibt die Kategorie-Farben zurueck
function GuildCompetition:GetCategoryColors()
    return CATEGORY_COLORS
end

--- Gibt die Todes-Strafpunkte zurueck
function GuildCompetition:GetDeathPenaltyTotal()
    return guildDeathPenalty
end

--- Gibt die Gesamtzahl der Tode zurueck die Strafpunkte verursacht haben
function GuildCompetition:GetDeathCount()
    if POINTS.member_death == 0 then return 0 end
    return math_floor(guildDeathPenalty / math.abs(POINTS.member_death))
end

-- ══════════════════════════════════════════════════════════════
-- CHAT-AUSGABE (fuer /gdl guildcomp)
-- ══════════════════════════════════════════════════════════════

function GuildCompetition:PrintStats()
    local myGuild = GDL.guildName or (GetGuildInfo("player")) or "?"
    local score = self:GetGuildScore()
    local rank, total = self:GetGuildRank()

    GDL:Print("|cffFFD100" .. GDL:T("═══ Gilden-Wettbewerb ═══") .. "|r")
    GDL:Print(string_format(GDL:T("Gilde: |cff00FF00%s|r"), myGuild))
    GDL:Print(string_format(GDL:T("Punkte: |cffFFD100%s|r"), self:FormatNumber(score)))
    GDL:Print(string_format(GDL:T("Rang: |cffFFD100#%d|r von %d bekannten Gilden"), rank, total))
    GDL:Print(string_format(GDL:T("Todes-Abzuege: |cffFF4040-%s|r (%d Tode)"),
        self:FormatNumber(guildDeathPenalty), self:GetDeathCount()))

    -- Top-3 Spieler
    local players = self:GetPlayerRanking()
    if #players > 0 then
        GDL:Print("|cffFFD100" .. GDL:T("--- Top Spieler ---") .. "|r")
        for i = 1, math.min(3, #players) do
            local p = players[i]
            GDL:Print(string_format(GDL:T("  %d. %s - %s Pkt (%d Kills)"),
                i, p.name, self:FormatNumber(p.points), p.totalKills))
        end
    end

    -- Leaderboard
    local lb = self:GetSortedLeaderboard()
    if #lb > 1 then
        GDL:Print("|cffFFD100--- Leaderboard ---|r")
        for i, entry in ipairs(lb) do
            local marker = entry.isOwn and (" |cff00FF00<< " .. GDL:T("DU") .. "|r") or ""
            GDL:Print(string_format(GDL:T("  #%d %s - %s Pkt%s"),
                i, entry.name, self:FormatNumber(entry.points), marker))
        end
    end
end

--- Formatiert eine Zahl mit Tausender-Trennung
function GuildCompetition:FormatNumber(n)
    if not n then return "0" end
    n = math_floor(n)
    if n < 1000 then return tostring(n) end

    local str = tostring(n)
    local result = ""
    local len = #str
    for i = 1, len do
        result = result .. str:sub(i, i)
        if (len - i) % 3 == 0 and i ~= len then
            result = result .. "."
        end
    end
    return result
end

-- ══════════════════════════════════════════════════════════════
-- INITIALISIERUNG & EVENTS
-- ══════════════════════════════════════════════════════════════

function GuildCompetition:OnInitialize()
    -- Nichts hier, warten auf PLAYER_READY
end

function GuildCompetition:OnPlayerReady()
    self:LoadState()
    self:RegisterChannel()
    self:StartSyncTimer()
    initialized = true
end

function GuildCompetition:OnEvent(event, ...)
    if event == "PLAYER_READY" then
        self:OnPlayerReady()
    elseif event == "CHAT_MSG_ADDON" then
        local prefix, msg, channel, sender = ...
        self:HandleAddonMessage(prefix, msg, channel, sender)
    end
end

-- ── Event-Frame: CHAT_MSG_ADDON + PLAYER_LOGOUT ─────────────
local eventFrame = CreateFrame("Frame")
eventFrame:RegisterEvent("CHAT_MSG_ADDON")
eventFrame:RegisterEvent("PLAYER_LOGOUT")
eventFrame:RegisterEvent("PLAYER_LEAVING_WORLD")
eventFrame:SetScript("OnEvent", function(_, event, ...)
    if event == "CHAT_MSG_ADDON" then
        GuildCompetition:OnEvent(event, ...)
    elseif event == "PLAYER_LOGOUT" or event == "PLAYER_LEAVING_WORLD" then
        -- Immer speichern beim Ausloggen!
        if initialized then
            GuildCompetition:SaveState()
        end
    end
end)

-- ══════════════════════════════════════════════════════════════
-- MODUL-REGISTRIERUNG
-- ══════════════════════════════════════════════════════════════
GDL:RegisterModule("GuildCompetition", GuildCompetition)
