-- ══════════════════════════════════════════════════════════════
-- MODUL: Guild – Erweitertes GRM-Pattern Gildenverwaltung
-- Multi-Guild Snapshot-Diff, Join/Leave-History, Rate-Limiting
-- Inspiriert von GuildRosterManager (TheGeneticsGuy)
-- ══════════════════════════════════════════════════════════════

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

-- ── Lokale API-Referenzen (Performance) ─────────────────────────
local GetNumGuildMembers      = GetNumGuildMembers
local GetGuildRosterInfo      = GetGuildRosterInfo
local GetGuildRosterLastOnline = GetGuildRosterLastOnline
local GetGuildInfo            = GetGuildInfo
local GetRealmName            = GetRealmName
local IsInGuild               = IsInGuild
local time                    = time
local strsplit                = strsplit
local math_abs                = math.abs
local string_format           = string.format
local string_lower            = string.lower

-- ── Konstanten ──────────────────────────────────────────────────
local MIN_SCAN_INTERVAL   = 3     -- Mindestsekunden zwischen Scans
local SCAN_QUEUE_DELAY    = 0.25  -- Batch-Verzögerung für Live-Events
local HEARTBEAT_TIMEOUT   = 10    -- Sekunden bis hängender Scan abgebrochen
local MIN_ROSTER_FOR_DIFF = 3     -- Mindestmitglieder für Diff (verhindert false leaves)

-- ── Runtime-State ───────────────────────────────────────────────
local scanInProgress      = false
local lastScanTime        = 0
local scanQueuePending    = false
local scanHeartbeatTimer  = nil
local rosterSnapshot      = {}    -- name_lower → memberData (aktueller Stand)
local eventQueue          = {}    -- Gepufferte Join/Leave-Events zur Batch-Verarbeitung

-- ═══════════════════════════════════════════════════════════════
-- INITIALISIERUNG
-- ═══════════════════════════════════════════════════════════════

function Guild:Initialize()
    self.eventFrame = CreateFrame("Frame")
    self.eventFrame:RegisterEvent("GUILD_ROSTER_UPDATE")
    self.eventFrame:RegisterEvent("PLAYER_GUILD_UPDATE")
    self.eventFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
    self.eventFrame:RegisterEvent("GUILD_MOTD")
    self.eventFrame:SetScript("OnEvent", function(_, event) self:OnEvent(event) end)

    C_Timer.After(4, function()
        if IsInGuild() then
            self:UpdateGuildInfo()
            GuildRoster()
            C_Timer.After(1.5, function() self:ScanRoster(true) end)
        end
    end)
end

function Guild:OnEvent(event)
    if event == "PLAYER_ENTERING_WORLD" then
        C_Timer.After(3, function()
            if IsInGuild() then
                self:UpdateGuildInfo()
                GuildRoster()
                C_Timer.After(1.5, function() self:ScanRoster(true) end)
            end
        end)

    elseif event == "GUILD_ROSTER_UPDATE" then
        self:QueueScan()

    elseif event == "PLAYER_GUILD_UPDATE" then
        C_Timer.After(1, function()
            if IsInGuild() then
                self:UpdateGuildInfo()
                GuildRoster()
                C_Timer.After(1, function() self:QueueScan() end)
            else
                self:UpdateGuildInfo()
            end
        end)

    elseif event == "GUILD_MOTD" then
        self:QueueScan()
    end
end

-- ═══════════════════════════════════════════════════════════════
-- RATE LIMITING & SCAN QUEUE
-- ═══════════════════════════════════════════════════════════════

-- Batcht mehrere schnelle Events zu einem einzigen Scan (wie GRM's LiveScanQue)
function Guild:QueueScan()
    if scanQueuePending then return end
    scanQueuePending = true
    C_Timer.After(SCAN_QUEUE_DELAY, function()
        scanQueuePending = false
        self:ScanRoster(false)
    end)
end

-- Verhindert Scan-Spam und hängende Scan-Zustände (wie GRM's LogPrecheck)
function Guild:CanScan()
    local now = time()
    if scanInProgress then
        -- Heartbeat: Hängenden Scan erkennen und zurücksetzen
        if now - lastScanTime > HEARTBEAT_TIMEOUT then
            GDL:Debug("Guild: Scan-Timeout erkannt, Reset (Heartbeat)")
            scanInProgress = false
            return true
        end
        return false
    end
    if now - lastScanTime < MIN_SCAN_INTERVAL then
        return false
    end
    return true
end

-- ═══════════════════════════════════════════════════════════════
-- ROSTER SCAN – KERN (GRM-Pattern: Snapshot → Diff → Report)
-- ═══════════════════════════════════════════════════════════════

function Guild:ScanRoster(isInitial)
    if not IsInGuild() then return end
    if not self:CanScan() then
        if not isInitial then self:QueueScan() end
        return
    end

    scanInProgress = true
    lastScanTime   = time()

    local numMembers = GetNumGuildMembers()
    if numMembers < 1 then
        scanInProgress = false
        return
    end

    -- ── 1. Neuen Snapshot aufbauen ─────────────────────────────
    local newSnapshot = {}
    for i = 1, numMembers do
        local name, rank, rankIndex, level, class, zone,
              note, officerNote, online, status, classFileName,
              achievementPoints, _, _, isMobile = GetGuildRosterInfo(i)

        if name then
            local baseName = strsplit("-", name)
            if baseName and baseName ~= "" then
                local key = string_lower(baseName)
                -- Last Online (nur für Offline-Mitglieder sinnvoll)
                local loYears, loMonths, loDays, loHours = 0, 0, 0, 0
                if not online and GetGuildRosterLastOnline then
                    local ok, y, m, d, h = pcall(GetGuildRosterLastOnline, i)
                    if ok then
                        loYears  = y or 0
                        loMonths = m or 0
                        loDays   = d or 0
                        loHours  = h or 0
                    end
                end
                newSnapshot[key] = {
                    name              = baseName,
                    rank              = rank or "",
                    rankIndex         = rankIndex or 0,
                    level             = level or 0,
                    class             = classFileName or class or "",
                    zone              = zone or "",
                    note              = note or "",
                    officerNote       = officerNote or "",
                    online            = online or false,
                    status            = status or 0,  -- 0=offline,1=online,2=AFK,3=DND
                    isMobile          = isMobile or false,
                    achievementPoints = achievementPoints or 0,
                    lastSeen          = online and time() or nil,
                    lastOnlineYears   = loYears,
                    lastOnlineMonths  = loMonths,
                    lastOnlineDays    = loDays,
                    lastOnlineHours   = loHours,
                }
            end
        end
    end

    -- ── 2. Diff: Join/Leave-Erkennung ──────────────────────────
    if not isInitial and #rosterSnapshot > 0 or next(rosterSnapshot) ~= nil then
        self:DiffRoster(rosterSnapshot, newSnapshot)
    end

    -- ── 3. Snapshot committen + SavedVariables aktualisieren ───
    rosterSnapshot = newSnapshot
    self:CommitSnapshot(newSnapshot)

    scanInProgress = false

    -- ── 4. C_Club Rassen/Berufsdaten nachladen (optional, async) ──
    C_Timer.After(2, function() self:FetchClubData() end)

    GDL:Debug(string_format("Guild: Scan abgeschlossen – %d Mitglieder", numMembers))
end

-- ── Diff zwischen altem und neuem Snapshot ────────────────────
-- Erkennt: Beigetretene (Join) und verschwundene (Leave/Tod) Mitglieder
function Guild:DiffRoster(oldSnap, newSnap)
    local oldCount, newCount = 0, 0
    for _ in pairs(oldSnap) do oldCount = oldCount + 1 end
    for _ in pairs(newSnap) do newCount = newCount + 1 end

    -- Sicherheitsprüfung: Zu kleiner Roster → kein Diff (verhindert mass-leave false positives)
    if newCount < MIN_ROSTER_FOR_DIFF then
        GDL:Debug("Guild: Diff übersprungen (Roster zu klein: " .. newCount .. ")")
        return
    end

    -- Joins erkennen (in newSnap, nicht in oldSnap)
    for key, newMember in pairs(newSnap) do
        if not oldSnap[key] then
            self:ProcessJoin(newMember)
        end
    end

    -- Leaves erkennen (in oldSnap, nicht in newSnap)
    for key, oldMember in pairs(oldSnap) do
        if not newSnap[key] then
            self:ProcessLeave(oldMember)
        end
    end

    -- Rang-Änderungen erkennen (zusätzliche GRM-Funktion)
    for key, newMember in pairs(newSnap) do
        local old = oldSnap[key]
        if old and old.rankIndex ~= newMember.rankIndex then
            self:ProcessRankChange(old, newMember)
        end
    end
end

-- ── Join verarbeiten ──────────────────────────────────────────
function Guild:ProcessJoin(memberInfo)
    local guildData = GDL:GetGuildData()
    if not guildData then return end

    guildData.memberHistory = guildData.memberHistory or {}

    local key = string_lower(memberInfo.name)
    local hist = guildData.memberHistory[key] or {}
    hist.name       = memberInfo.name
    hist.class      = memberInfo.class
    hist.joinDate   = hist.joinDate or time()  -- Ersten Join-Zeitpunkt behalten
    hist.lastJoin   = time()
    hist.leaveDate  = nil
    hist.leaveReason = nil
    hist._removed   = false
    guildData.memberHistory[key] = hist

    GDL:Debug("Guild: JOIN erkannt – " .. memberInfo.name ..
        " (Rang: " .. memberInfo.rank .. ", Level: " .. memberInfo.level .. ")")
end

-- ── Leave verarbeiten ─────────────────────────────────────────
-- WICHTIG: Ein Gilden-Austritt bedeutet KEIN Tod.
-- Todeserkennung läuft ausschließlich über UNIT_DIED / Deathlog.lua.
-- Hier wird nur die Mitglieder-History gepflegt.
function Guild:ProcessLeave(memberInfo)
    local guildData = GDL:GetGuildData()
    if not guildData then return end

    guildData.memberHistory = guildData.memberHistory or {}
    guildData.members       = guildData.members or {}

    local key = string_lower(memberInfo.name)
    local now = time()

    -- memberHistory: Austritts-Zeitpunkt und Grund festhalten
    local hist = guildData.memberHistory[key] or {}
    hist.name       = memberInfo.name
    hist.class      = memberInfo.class or hist.class
    hist.leaveDate  = now
    hist._removed   = true
    hist._removedAt = now
    hist.online     = false
    guildData.memberHistory[key] = hist

    -- guildData.members: als "ausgetreten" markieren (nicht löschen, für History-Anzeige)
    local member = guildData.members[key]
    if type(member) == "table" then
        member._removed   = true
        member._removedAt = now
        member.online     = false
    end

    GDL:Debug("Guild: LEAVE erkannt (kein Tod) – " .. memberInfo.name)
end

-- ── Rang-Änderung verarbeiten ─────────────────────────────────
function Guild:ProcessRankChange(oldMember, newMember)
    local guildData = GDL:GetGuildData()
    if not guildData then return end

    guildData.memberHistory = guildData.memberHistory or {}
    local key = string_lower(newMember.name)
    local hist = guildData.memberHistory[key] or {}

    hist.rankHistory = hist.rankHistory or {}
    table.insert(hist.rankHistory, {
        fromRank   = oldMember.rank,
        toRank     = newMember.rank,
        fromIndex  = oldMember.rankIndex,
        toIndex    = newMember.rankIndex,
        timestamp  = time(),
    })
    -- Nur die letzten 20 Rang-Änderungen behalten (Speicher-Limit)
    while #hist.rankHistory > 20 do table.remove(hist.rankHistory, 1) end

    hist.rank      = newMember.rank
    hist.rankIndex = newMember.rankIndex
    guildData.memberHistory[key] = hist

    GDL:Debug(string_format("Guild: Rang-Änderung – %s: %s → %s",
        newMember.name, oldMember.rank, newMember.rank))
end

-- ═══════════════════════════════════════════════════════════════
-- SNAPSHOT → SAVEDVARIABLES COMMIT
-- ═══════════════════════════════════════════════════════════════

function Guild:CommitSnapshot(snapshot)
    local guildData = GDL:GetGuildData()
    if not guildData then return end

    guildData.members = guildData.members or {}
    guildData.memberHistory = guildData.memberHistory or {}

    local now = time()
    local count = 0

    -- ── 1. Aktuelle Mitglieder mergen / updaten ──────────────────
    for key, memberInfo in pairs(snapshot) do
        local existing = guildData.members[key]
        if type(existing) ~= "table" then existing = {} end
        existing.name        = memberInfo.name
        existing.rank        = memberInfo.rank
        existing.rankIndex   = memberInfo.rankIndex
        existing.level       = memberInfo.level or existing.level
        existing.class       = memberInfo.class or existing.class
        existing.zone        = memberInfo.online and memberInfo.zone or existing.zone
        existing.online      = memberInfo.online
        existing.note        = memberInfo.note
        existing.lastSeen    = memberInfo.online and now or existing.lastSeen
        existing._removed    = nil    -- Wieder beigetreten → Removed-Flag löschen
        existing._removedAt  = nil
        guildData.members[key] = existing
        count = count + 1
    end

    -- ── 2. Verschwundene Mitglieder als ausgetreten markieren ────
    -- (Werden bei Offline-Leave nicht von ProcessLeave erfasst)
    for key, member in pairs(guildData.members) do
        if type(member) == "table" and not member._removed then
            if not snapshot[key] then
                member._removed   = true
                member._removedAt = member._removedAt or now
                member.online     = false
                GDL:Debug("Guild: CommitSnapshot markiert als ausgetreten – " .. (member.name or key))
            end
        end
    end

    GDL:Debug("Guild: CommitSnapshot – " .. count .. " Einträge persistiert")
end

-- ═══════════════════════════════════════════════════════════════
-- GILDENVERWALTUNG
-- ═══════════════════════════════════════════════════════════════

function Guild:UpdateGuildInfo()
    local oldGuild = GDL.currentGuildName

    if IsInGuild() then
        local guildName = GetGuildInfo("player")
        if guildName and guildName ~= "" then
            GDL.currentGuildName = guildName

            if oldGuild and oldGuild ~= GDL.currentGuildName then
                GDL:Print(string_format(GDL:L("GUILD_CHANGED"), GDL.currentGuildName))
                -- Snapshot zurücksetzen bei Gildenwechsel
                rosterSnapshot = {}
            end

            -- Realm für Multi-Guild-Key speichern
            GDL.currentRealm = GetRealmName() or ""
        end
    else
        if oldGuild then GDL:Print(GDL:L("GUILD_LEFT")) end
        GDL.currentGuildName = nil
        rosterSnapshot = {}
    end

    local UI = GDL:GetModule("UI")
    if UI and UI.mainFrame and UI.mainFrame:IsShown() then
        UI:UpdateChronicle()
    end
end

-- Eindeutiger Schlüssel für Guild+Realm-Kombination (GRM-Pattern)
function Guild:GetGuildKey()
    local guildName = GDL.currentGuildName or "Unknown"
    local realm     = GDL.currentRealm or GetRealmName() or "Unknown"
    return realm .. "_" .. guildName
end

-- ═══════════════════════════════════════════════════════════════
-- ABFRAGE-API (IsMember, GetMemberInfo, etc.)
-- ═══════════════════════════════════════════════════════════════

-- Primäre Mitglieder-Prüfung: Zuerst Runtime-Snapshot, dann SavedVariables
function Guild:IsMember(name)
    if not name then return false end

    local cleanName = strsplit("-", name)
    if not cleanName or cleanName == "" then return false end
    local key = string_lower(cleanName)

    -- 1. Schnell-Check im Runtime-Snapshot (kein SavedVariables-Overhead)
    if rosterSnapshot[key] then return true end

    -- 2. Fallback: SavedVariables (nach Reload/Relog noch verfügbar)
    local guildData = GDL:GetGuildData()
    if guildData and guildData.members then
        local member = guildData.members[key]
        if type(member) == "table" and not member._removed then
            return true
        end
    end

    -- 3. Live-API: Für sehr neue Mitglieder (noch nicht gescannt)
    if IsInGuild() then
        local numMembers = GetNumGuildMembers()
        for i = 1, numMembers do
            local rName = GetGuildRosterInfo(i)
            if rName then
                local rBase = strsplit("-", rName)
                if rBase and string_lower(rBase) == key then
                    GDL:Debug("Guild:IsMember – Live-API Treffer: " .. cleanName)
                    return true
                end
            end
        end
    end

    GDL:Debug("Guild:IsMember('" .. name .. "') = false")
    return false
end

function Guild:GetMemberInfo(name)
    if not name then return nil end
    local cleanName = strsplit("-", name)
    if not cleanName or cleanName == "" then return nil end
    local key = string_lower(cleanName)

    -- Runtime-Snapshot hat frischeste Daten
    if rosterSnapshot[key] then return rosterSnapshot[key] end

    -- Fallback SavedVariables
    local guildData = GDL:GetGuildData()
    if guildData and guildData.members then
        local info = guildData.members[key]
        if type(info) == "table" then return info end
    end
    return nil
end

function Guild:GetMemberHistory(name)
    if not name then return nil end
    local cleanName = strsplit("-", name)
    local key = string_lower(cleanName)
    local guildData = GDL:GetGuildData()
    if guildData and guildData.memberHistory then
        return guildData.memberHistory[key]
    end
    return nil
end

function Guild:GetAllMembers()
    -- Runtime-Snapshot bevorzugen (immer frisch)
    if next(rosterSnapshot) then return rosterSnapshot end
    local guildData = GDL:GetGuildData()
    if guildData and guildData.members then return guildData.members end
    return {}
end

-- ── C_Club Rassen & Berufs-Daten (Communities API, Classic Era +2023) ──
-- Nur additiv: füllt fehlende Felder (race, raceId, clubProf1, clubProf2, sex)
-- ohne vorhandene Werte zu überschreiben
function Guild:FetchClubData()
    if not C_Club then return end
    local ok1, clubID = pcall(C_Club.GetGuildClubId)
    if not ok1 or not clubID then return end

    -- GetClubMembers gibt eine Liste von memberIDs (Zahlen) zurück,
    -- NICHT die MemberInfo-Objekte direkt!
    local ok2, memberIDs = pcall(C_Club.GetClubMembers, clubID)
    if not ok2 or not memberIDs then return end

    local guildData = GDL:GetGuildData()
    if not guildData or not guildData.members then return end

    local updated = 0
    for _, memberID in ipairs(memberIDs) do
        -- Für jede memberID die eigentlichen Infos abrufen
        local ok3, memberInfo = pcall(C_Club.GetMemberInfo, clubID, memberID)
        if ok3 and memberInfo and type(memberInfo) == "table" and memberInfo.name then
            local key = string_lower(strsplit("-", memberInfo.name))
            local m    = guildData.members[key]
            local snap = rosterSnapshot[key]

            if type(m) == "table" then
                -- Rasse
                if memberInfo.race and memberInfo.race > 0 and not m.raceId then
                    m.raceId = memberInfo.race
                    if C_CreatureInfo and C_CreatureInfo.GetRaceInfo then
                        local ok4, raceInfo = pcall(C_CreatureInfo.GetRaceInfo, memberInfo.race)
                        if ok4 and raceInfo then m.race = raceInfo.raceName end
                    end
                end
                -- Geschlecht (2=männlich, 3=weiblich)
                if memberInfo.sex and memberInfo.sex > 0 and not m.sex then
                    m.sex = memberInfo.sex
                end
                -- Berufe
                if memberInfo.profession1ID and memberInfo.profession1ID > 0 then
                    m.clubProf1 = { id = memberInfo.profession1ID, rank = memberInfo.profession1Rank or 0 }
                end
                if memberInfo.profession2ID and memberInfo.profession2ID > 0 then
                    m.clubProf2 = { id = memberInfo.profession2ID, rank = memberInfo.profession2Rank or 0 }
                end
                -- In Runtime-Snapshot übertragen
                if snap and type(snap) == "table" then
                    snap.raceId    = m.raceId
                    snap.race      = m.race
                    snap.sex       = m.sex
                    snap.clubProf1 = m.clubProf1
                    snap.clubProf2 = m.clubProf2
                end
                updated = updated + 1
            end
        end
    end
    if updated > 0 then
        GDL:Debug("Guild: FetchClubData – " .. updated .. " Einträge aktualisiert")
    end
end

function Guild:GetMemberCount()
    -- Snapshot-Count ist zuverlässiger als SavedVariables-Count
    local count = 0
    for _ in pairs(rosterSnapshot) do count = count + 1 end
    if count > 0 then return count end

    local guildData = GDL:GetGuildData()
    if not guildData or not guildData.members then return 0 end
    for _ in pairs(guildData.members) do count = count + 1 end
    return count
end

-- Online-Mitglieder aus aktuellem Snapshot
function Guild:GetOnlineMembers()
    local online = {}
    for key, member in pairs(rosterSnapshot) do
        if member.online then
            online[key] = member
        end
    end
    return online
end

-- Rang-basierte Abfrage (für UI-Anzeige)
function Guild:GetMembersByRank(rankIndex)
    local result = {}
    for key, member in pairs(rosterSnapshot) do
        if member.rankIndex == rankIndex then
            result[key] = member
        end
    end
    return result
end

-- ═══════════════════════════════════════════════════════════════
-- DEBUG & DIAGNOSE
-- ═══════════════════════════════════════════════════════════════

function Guild:GetDiagnostics()
    local snapCount = 0
    for _ in pairs(rosterSnapshot) do snapCount = snapCount + 1 end

    local guildData = GDL:GetGuildData()
    local savedCount = 0
    local historyCount = 0
    if guildData then
        for _ in pairs(guildData.members or {}) do savedCount = savedCount + 1 end
        for _ in pairs(guildData.memberHistory or {}) do historyCount = historyCount + 1 end
    end

    return {
        snapshotSize   = snapCount,
        savedSize      = savedCount,
        historySize    = historyCount,
        scanInProgress = scanInProgress,
        lastScanTime   = lastScanTime,
        guildKey       = self:GetGuildKey(),
    }
end

GDL:RegisterModule("Guild", Guild)
