---@diagnostic disable: undefined-field
local addOnName, PUP = ...

local LibSerialize = LibStub("LibSerialize")
local LibDeflate = LibStub("LibDeflate")
local LDBIcon = LibStub("LibDBIcon-1.0")
local AceComm = LibStub("AceComm-3.0")

local serializedTable
local spacing = 4
local lastUpdate = 0
local updateQueued = false

local factionID = 2685
local questIDs       = { 89018, 89019, 89020, 89021, 89022, 89023 }
local thresholds     = { 4, 7, 10, 13, 16, 19 } 
local startDateTable = { year = 2025, month = 2, day = 26, hour = 6 }

local mrtUpdateTimer

local auraImportElementPool = {}
local UIDToID = {} -- Installed aura UIDs to ID (ID is required for WeakAuras.GetData call)
local auraUIDs = {} -- UIDs of AuraUpdater auras
local guidToVersionsTable = {}

local allAurasUpdatedText
local playerVersionsTable -- Table containing all the version information. Serialized before sent to others. Used as-is for displaying our own versions.
local UpdateVersionsForUnit = function(_, _) end

function PUP:GetInstalledAuraDataByUID(uid)
    local installedAuraID = UIDToID[uid]
    
    return installedAuraID and WeakAuras.GetData(installedAuraID)
end

function PUP:GetVersionsTableForGUID(GUID)
    return guidToVersionsTable[GUID]
end

function PUP:UpdateVersionsTableForGUID(GUID, versionsTable)
    guidToVersionsTable[GUID] = versionsTable
end

-- Berechnet die aktuelle Woche seit startDateTable (Wochenbeginn: Mittwoch)
local function getCurrentWeek(startDateTable)
    local startTS = time(startDateTable)
    local nowTS   = time()             -- WoW-API: entspricht os.time()
    local shift   = 2 * 24 * 60 * 60   -- 2 Tage, um von Dienstag→Mittwoch zu verschieben
    local weekSec = 7 * 24 * 60 * 60
  
    local diff = nowTS + shift - startTS
    if diff < 0 then return 0 end
    return math.floor(diff / weekSec) + 1
end
  
  -- Ermittelt, wie viele der aktuell verfügbaren Quest-IDs bereits abgeschlossen sind
local function getCompletedQuestCount(currentWeek)
    local unlockedQuests = {}
    for i, weekAvail in ipairs(thresholds) do
      if currentWeek >= weekAvail then
        table.insert(unlockedQuests, questIDs[i])
      end
    end
    local count = 0
    for _, qid in ipairs(unlockedQuests) do
      if C_QuestLog.IsQuestFlaggedCompleted(qid) then
        count = count + 1
      end
    end
    return #unlockedQuests, count
end

local function BroadcastVersions()
    local chatType = IsInGroup(LE_PARTY_CATEGORY_INSTANCE) and "INSTANCE_CHAT" or IsInRaid() and "RAID" or "PARTY"

    AceComm:SendCommMessage("PU_Versions", serializedTable, "GUILD")
    AceComm:SendCommMessage("PU_Versions", serializedTable, chatType)

    UpdateVersionsForUnit(playerVersionsTable, "player")
end

local function SerializeVersionsTable()
    for displayName, auraData in pairs(PrepullUpdaterSaved.WeakAuras) do
        local uid = auraData.d.uid
        local installedAuraID = uid and UIDToID[uid]
        local installedVersion = installedAuraID and WeakAuras.GetData(installedAuraID).prepullVersion or 0

        playerVersionsTable.auras[displayName] = installedVersion
    end
	
    local factionData = C_MajorFactions.GetMajorFactionData(factionID)
    playerVersionsTable.garbagioRenown = factionData and factionData.renownLevel or 0


    local currentWeek = getCurrentWeek(startDateTable)

    playerVersionsTable.questsUnlocked, playerVersionsTable.completedQuests = getCompletedQuestCount(currentWeek)
    playerVersionsTable.maxRenownPossible = math.min(currentWeek, 19)

    local serialized = LibSerialize:Serialize(playerVersionsTable)
    local compressed = LibDeflate:CompressDeflate(serialized, {level = 9})
    local encoded = LibDeflate:EncodeForWoWAddonChannel(compressed)

    serializedTable = encoded

    if not serializedTable then
        PUP:ErrorPrint("could not serialize version table")
    end
end


-- Returns true if a new group member was ignored
local function UpdateIgnoredPlayers()
    local foundNew = false
    local newIgnoredNames = {}

    for unit in PUP:IterateGroupMembers() do
        if C_FriendList.IsIgnored(unit) then
            local name = UnitNameUnmodified(unit)

            table.insert(newIgnoredNames, name)
        end
    end

    table.sort(newIgnoredNames)

    if not tCompare(newIgnoredNames, playerVersionsTable.ignores) then
        foundNew = true
    end

    playerVersionsTable.ignores = newIgnoredNames

    return foundNew
end

-- Calculates checksum for the player's public MRT note
-- Original code by Mikk (https://warcraft.wiki.gg/wiki/StringHash)
local function GetMRTNoteHash()
    local text = VMRT and VMRT.Note.Text1

    if not text then return end

    local counter = 1
    local len = string.len(text)

    for i = 1, len, 3 do 
        counter = math.fmod(counter * 8161, 4294967279) + (string.byte(text, i) * 16776193) + ((string.byte(text, i + 1) or (len - i + 256)) * 8372226) + ((string.byte(text, i + 2) or (len - i + 256)) * 3932164)
    end

    return math.fmod(counter, 4294967291)
end

-- Returns true if a new MRT note was found
local function UpdateMRTNoteHash()
    if not C_AddOns.IsAddOnLoaded("MRT") then return end

    local foundNew = false
    local hash = GetMRTNoteHash()

    if playerVersionsTable.mrtNoteHash ~= hash then
        foundNew = true
    end

    playerVersionsTable.mrtNoteHash = hash

    return foundNew
end

local function UpdateGarbagioRenown()
    
    local factionData = C_MajorFactions.GetMajorFactionData(factionID)
    local renownLevel = factionData and factionData.renownLevel or 0

    if playerVersionsTable.garbagioRenown ~= renownLevel then
        SerializeVersionsTable()
        BroadcastVersions()
    end
end

function PUP:UpdateMinimapIconVisibility()
    if PUP.upToDate then
        if PrepullUpdaterSaved.settings.hideMinimapIcon then
            LDBIcon:Hide("Prepull Updater")
        else
            LDBIcon:Show("Prepull Updater")
        end

        PUP.LDB.icon = [[Interface\Addons\PrepullUpdater\Media\Textures\minimap_logo.tga]]
    else
        LDBIcon:Show("Prepull Updater")

        PUP.LDB.icon = [[Interface\Addons\PrepullUpdater\Media\Textures\minimap_logo_red.tga]]
    end
end

local function BuildAuraImportElements()
    lastUpdate = GetTime()
    updateQueued = false

    -- Check if addon requires an update
    local addOnVersionsBehind = PUP.highestSeenVersionsTable.addOn - playerVersionsTable.addOn

    -- Check which auras require updates
    local aurasToUpdate = {}

    for displayName, highestSeenVersion in pairs(PUP.highestSeenVersionsTable.auras) do
        local auraData = PrepullUpdaterSaved.WeakAuras[displayName]
        local uid = auraData and auraData.d.uid
        local importedVersion = auraData and auraData.d.prepullVersion or 0
        local installedAuraID = uid and UIDToID[uid]
        local installedVersion = installedAuraID and WeakAuras.GetData(installedAuraID).prepullVersion or 0

        if installedVersion < importedVersion then
            table.insert(
                aurasToUpdate,
                {
                    displayName = displayName,
                    installedVersion = installedVersion,
                    importedVersion = importedVersion,
                    highestSeenVersion = highestSeenVersion
                }
            )
        end
    end

    table.sort(
        aurasToUpdate,
        function(auraData1, auraData2)
            local versionsBehind1 = auraData1.highestSeenVersion - auraData1.installedVersion
            local versionsBehind2 = auraData2.highestSeenVersion - auraData2.installedVersion

            if versionsBehind1 ~= versionsBehind2 then
                return versionsBehind1 > versionsBehind2
            else
                return auraData1.displayName < auraData2.displayName
            end
        end
    )

    -- Build the aura import elements
    local parent = PUP.updateWindow

    for _, element in ipairs(auraImportElementPool) do
        element:Hide()
    end

    -- AddOn element
    if addOnVersionsBehind > 0 then
        local auraImportFrame = auraImportElementPool[1] or PUP:CreateAuraImportElement(parent)

        auraImportFrame:SetDisplayName("Prepull Updater")
        auraImportFrame:SetVersionsBehind(addOnVersionsBehind)
        auraImportFrame:SetRequiresAddOnUpdate(true)

        auraImportFrame:Show()
        auraImportFrame:SetPoint("TOPLEFT", parent, "TOPLEFT", spacing, -spacing)
        auraImportFrame:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -spacing, -spacing)
        
        auraImportElementPool[1] = auraImportFrame
    end

    -- Aura elements
    for index, auraData in ipairs(aurasToUpdate) do
        -- If the addon requires an update, the first element indicates that
        -- Aura updates should use subsequent elements
        local i = addOnVersionsBehind > 0 and index + 1 or index
        local auraImportFrame = auraImportElementPool[i] or PUP:CreateAuraImportElement(parent)

        auraImportFrame:SetDisplayName(auraData.displayName)
        auraImportFrame:SetVersionsBehind(auraData.highestSeenVersion - auraData.installedVersion)
        auraImportFrame:SetRequiresAddOnUpdate(auraData.highestSeenVersion > auraData.importedVersion)

        auraImportFrame:Show()
        auraImportFrame:SetPoint("TOPLEFT", parent, "TOPLEFT", spacing, -(i - 1) * (auraImportFrame.height + spacing) - spacing)
        auraImportFrame:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -spacing, -(i - 1) * (auraImportFrame.height + spacing) - spacing)
        
        auraImportElementPool[i] = auraImportFrame
    end

    PUP.upToDate = addOnVersionsBehind <= 0 and next(aurasToUpdate) == nil

    allAurasUpdatedText:SetShown(PUP.upToDate)

    PUP:UpdateMinimapIconVisibility()
end

function PUP:QueueUpdate()
    if updateQueued then return end

    -- Don't update more than once per second
    -- This is mostly to prevent the update function from running when a large number of auras get added simultaneously
    local timeSinceLastUpdate = GetTime() - lastUpdate

    if timeSinceLastUpdate > 1 then
        BuildAuraImportElements()
    else
        updateQueued = true

        C_Timer.After(1 - timeSinceLastUpdate, BuildAuraImportElements)
    end
end

local function RequestVersions(chatType)
    AceComm:SendCommMessage("PU_Request", " ", chatType or "GUILD")
end

UpdateVersionsForUnit = function(versionsTable, unit)
    local shouldFullRebuildAura = false
    local shouldFullRebuildOther = false
    local GUID = UnitGUID(unit)

    if not GUID then return end

    -- Check addon version
    local addOnVersion = versionsTable.addOn
    local highestSeenAddOnVersion = PUP.highestSeenVersionsTable.addOn

    if not highestSeenAddOnVersion or highestSeenAddOnVersion < addOnVersion then
        PUP.highestSeenVersionsTable.addOn = addOnVersion

        shouldFullRebuildAura = true
    end

    -- Check aura versions
    for displayName, version in pairs(versionsTable.auras or {}) do
        local highestSeenVersion = PUP.highestSeenVersionsTable.auras[displayName]

        if highestSeenVersion and highestSeenVersion < version then
            PUP.highestSeenVersionsTable.auras[displayName] = version

            shouldFullRebuildAura = true
        end
    end

    -- Check RCLC version
    local RCLCVersion = versionsTable.RCLC

    if RCLCVersion then
        if not PUP.highestSeenRCLCVersion or PUP:CompareRCLCVersions(PUP.highestSeenRCLCVersion, RCLCVersion) == 1 then
            PUP.highestSeenRCLCVersion = RCLCVersion

            shouldFullRebuildOther = true
        end
    end

    if shouldFullRebuildAura then
        PUP:QueueUpdate()

        PUP.auraChecker:RebuildAllCheckElements()
    else
        PUP.auraChecker:UpdateCheckElementForUnit(unit, versionsTable)
    end

    if shouldFullRebuildOther then
        PUP.otherChecker:RebuildAllCheckElements()
    else
        PUP.otherChecker:UpdateCheckElementForUnit(unit, versionsTable)
    end

    PUP:UpdateVersionsTableForGUID(GUID, versionsTable)
end

local function ReceiveVersions(_, payload, _, sender)
    if UnitIsUnit(sender, "player") then return end -- We handle our own versions directly, not through addon messages

    local decoded = LibDeflate:DecodeForWoWAddonChannel(payload)

    if not decoded then
        PUP:ErrorPrint(string.format("could not decode version table received from %s", sender))

        return
    end

    local decompressed = LibDeflate:DecompressDeflate(decoded)

    if not decoded then
        PUP:ErrorPrint(string.format("could not decompress version table received from %s", sender))

        return
    end

    local success, versionsTable = LibSerialize:Deserialize(decompressed)

    if not success then
        PUP:ErrorPrint(string.format("could not deserialize version table received from %s", sender))

        return
    end

    -- If the auras subtable doesn't exist, that means the user still has an old Prepull Updater version
    -- Convert it to the new format
    if not versionsTable.auras then
        local newVersionsTable = {
            addOn = versionsTable["Prepull Updater"],
            auras = {}
        }

        for displayName, version in pairs(versionsTable) do
            if displayName ~= "Prepull Updater" then
                newVersionsTable.auras[displayName] = version
            end
        end

        versionsTable = newVersionsTable
    end

    UpdateVersionsForUnit(versionsTable, sender)
end

-- Called before updating an aura
-- Checks if the user already has the aura installed
-- If so, apply "load: never" settings from the existing aura (group) to the aura being imported
-- If "forceenable" is included in the description of an aura, always uncheck "load: never"
function PUP:ApplyLoadSettings(auraData, installedAuraData)
    if installedAuraData and installedAuraData.load and not (installedAuraData.regionType == "group" or installedAuraData.regionType == "dynamicgroup") then
        auraData.load.use_never = installedAuraData.load.use_never

        if auraData.desc and type(auraData.desc) == "string" and auraData.desc:match("forceenable") then
            auraData.load.use_never = nil
        end
    end
end

-- Similar to ApplyLoadSettings: preserves sound settings in action tab
function PUP:ApplySoundSettings(auraData, installedAuraData)
    if not (installedAuraData and installedAuraData.actions) then return end

    local start = installedAuraData.actions.start
    local finish = installedAuraData.actions.finish

    -- Preserve on show sounds
    if start then
        if not auraData.actions.start then auraData.actions.start = {} end

        auraData.actions.start.do_sound = start.do_sound
        auraData.actions.start.do_loop = start.do_loop
        auraData.actions.start.sound = start.sound
        auraData.actions.start.sound_channel = start.sound_channel
        auraData.actions.start.sound_repeat = start.sound_repeat
    end

    -- Preserve on hide sounds
    if finish then
        if not auraData.actions.finish then auraData.actions.finish = {} end

        auraData.actions.finish.do_sound = finish.do_sound
        auraData.actions.finish.do_sound_fade = finish.do_sound_fade
        auraData.actions.finish.sound = finish.sound
        auraData.actions.finish.sound_channel = finish.sound_channel
        auraData.actions.finish.stop_sound = finish.stop_sound
        auraData.actions.finish.stop_sound_fade = finish.stop_sound_fade
    end
end


-- Force updates the on init code, even if the user unchecked "actions" when importing
-- Users often do this to preserve their sounds/glow colors/etc. but it can break assignment functionality
function PUP:ForceUpdateOnInit(customOnInit)
    for id, customCode in pairs(customOnInit) do
        local data = WeakAuras.GetData(id)

        if data and data.actions and data.actions.init then
            data.actions.init.do_custom = true
            data.actions.init.custom = customCode
        end
    end
end

-- Called on callback from WeakAuras.Import (in AuraImportElement)
function PUP:OnUpdateAura()
    SerializeVersionsTable()
    
    PUP:QueueUpdate()

    BroadcastVersions()
end

function PUP:InitializeAuraUpdater()
    PUP.highestSeenVersionsTable = {
        addOn = tonumber(C_AddOns.GetAddOnMetadata(addOnName, "Version")),
        auras = {}
    }
    playerVersionsTable = {
        addOn = tonumber(C_AddOns.GetAddOnMetadata(addOnName, "Version")),
        auras = {},
        ignores = {},
		garbagioRenown = {},
        questsUnlocked = {},
        completedQuests = {},
        maxRenownPossible = {}
    }

    UpdateIgnoredPlayers()
    UpdateMRTNoteHash()

    -- RCLC version
    if C_AddOns.IsAddOnLoaded("RCLootCouncil") then
        local version = C_AddOns.GetAddOnMetadata("RCLootCouncil", "Version")

        playerVersionsTable.RCLC = version

        PUP.highestSeenRCLCVersion = version
    end
    
    AceComm:RegisterComm("PU_Request", BroadcastVersions)
    AceComm:RegisterComm("PU_Versions", ReceiveVersions)

    for displayName, auraData in pairs(PrepullUpdaterSaved.WeakAuras) do
        auraUIDs[auraData.d.uid] = true

        PUP.highestSeenVersionsTable.auras[displayName] = auraData.d.prepullVersion
    end

    if WeakAuras and WeakAurasSaved and WeakAurasSaved.displays then
        for id, auraData in pairs(WeakAurasSaved.displays) do
            UIDToID[auraData.uid] = id
        end

        hooksecurefunc(
            WeakAuras,
            "Add",
            function(data)
                local uid = data.uid

                if uid then
                    UIDToID[uid] = data.id
                end
            end
        )

        hooksecurefunc(
            WeakAuras,
            "Rename",
            function(data, newID)
                local uid = data.uid

                if uid then
                    UIDToID[uid] = newID
                end
            end
        )

        hooksecurefunc(
            WeakAuras,
            "Delete",
            function(data)
                local uid = data.uid

                if uid then
                    UIDToID[uid] = nil

                    if auraUIDs[uid] then
                        PUP:OnUpdateAura()
                    end
                end
            end
        )
    end

    if MRTNote and MRTNote.text then
        hooksecurefunc(
            MRTNote.text,
            "SetText",
            function()
                if mrtUpdateTimer and not mrtUpdateTimer:IsCancelled() then
                    mrtUpdateTimer:Cancel()
                end

                mrtUpdateTimer = C_Timer.NewTimer(
                    3,
                    function()
                        local shouldBroadcast = UpdateMRTNoteHash()

                        if shouldBroadcast then
                            SerializeVersionsTable()
                            BroadcastVersions()
                        end
                    end
                )
            end
        )
    end

    allAurasUpdatedText = PUP.updateWindow:CreateFontString(nil, "OVERLAY")

    allAurasUpdatedText:SetFontObject(PUFont21)
    allAurasUpdatedText:SetPoint("CENTER", PUP.updateWindow, "CENTER")
    allAurasUpdatedText:SetText(string.format("|cff%sAura auf dem neuesten Stand!|r", PUP.gs.visual.colorStrings.green))

    SerializeVersionsTable()

    PUP:QueueUpdate()

    -- For some reason the minimap icon doesn't hide if this code runs on the same frame it's being created
    -- In other words, if all auras are up to date, and the user hides the minimap icon, it doesn't hide on log (or reload)
    C_Timer.After(0, function() PUP:UpdateMinimapIconVisibility() end)
	C_Timer.After(1, UpdateGarbagioRenown)
	
	local renownFrame = CreateFrame("Frame")
	renownFrame:RegisterEvent("MAJOR_FACTION_RENOWN_LEVEL_CHANGED")
	renownFrame:SetScript("OnEvent", function(_, _, changedFactionID)
		if changedFactionID == factionID then
			SerializeVersionsTable()
			BroadcastVersions()
		end
	end)
end

local function OnEvent(_, event)
    if event == "GROUP_ROSTER_UPDATE" then
        PUP.auraChecker:RemoveCheckElementsForInvalidUnits()
        PUP.auraChecker:AddCheckElementsForNewUnits()

        PUP.otherChecker:RemoveCheckElementsForInvalidUnits()
        PUP.otherChecker:AddCheckElementsForNewUnits()

        if UpdateIgnoredPlayers() then
            SerializeVersionsTable()
            BroadcastVersions()
        end
    elseif event == "GROUP_JOINED" then
        local chatType = IsInGroup(LE_PARTY_CATEGORY_INSTANCE) and "INSTANCE_CHAT" or IsInRaid() and "RAID" or "PARTY"

        RequestVersions(chatType)
    elseif event == "PLAYER_ENTERING_WORLD" then
        local chatType = IsInGroup(LE_PARTY_CATEGORY_INSTANCE) and "INSTANCE_CHAT" or IsInRaid() and "RAID" or "PARTY"

        RequestVersions(chatType)
        RequestVersions() -- GUILD

        UpdateVersionsForUnit(playerVersionsTable, "player") -- Just in case the player is not in a guild/group
    elseif event == "IGNORELIST_UPDATE" then
        if UpdateIgnoredPlayers() then
            SerializeVersionsTable()
            BroadcastVersions()
        end
    elseif event == "READY_CHECK" then
        if PrepullUpdaterSaved.settings.readyCheckPopup and not PUP.upToDate then
            PUP:ShowPopupWindow(
                string.format("|cff%sWarnung|r|n|nDein Addon/Aura sind veraltet!", PUP.gs.visual.colorStrings.red),
                function(dontShowAgain)
                    PUP:SetNotifyOnReadyCheck(not dontShowAgain)
                end,
                0,
                150,
                true
            )
        end
    end
end

local f = CreateFrame("Frame")
f:RegisterEvent("GROUP_ROSTER_UPDATE")
f:RegisterEvent("GROUP_JOINED")
f:RegisterEvent("PLAYER_ENTERING_WORLD")
f:RegisterEvent("IGNORELIST_UPDATE")
f:RegisterEvent("READY_CHECK")
f:SetScript("OnEvent", OnEvent)