---@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 updatePopupWindow

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

local factionID             = 2736
local questIDs              = { 90018, 90019, 90020, 90021, 90022, 90023 }
local thresholds            = { 4, 7, 10, 13, 15 }
local startDateTable        = { year = 2025, month = 8, day = 6, 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 weekSec = 7 * 24 * 60 * 60

    local diff    = nowTS - 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.raidRenown = 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 UpdateRaidRenown()
    local factionData = C_MajorFactions.GetMajorFactionData(factionID)
    local renownLevel = factionData and factionData.renownLevel or 0

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

function PUP:UpdateMinimapIconVisibility()
    if PrepullUpdaterSaved.settings.hideMinimapIcon then
        LDBIcon:Hide("Prepull Updater")
    else
        LDBIcon:Show("Prepull Updater")
        if PUP.upToDate then
            PUP.LDB.icon = [[Interface\Addons\PrepullUpdater\Media\Textures\minimap_logo.tga]]
        else
            PUP.LDB.icon = [[Interface\Addons\PrepullUpdater\Media\Textures\minimap_logo_red.tga]]
        end
    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 < highestSeenVersion 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

    PUP:UpdateVersionsTableForGUID(GUID, versionsTable)

    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
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

local function UpdateRCLCVersion()
    local version = C_AddOns.GetAddOnMetadata("RCLootCouncil", "Version")

    playerVersionsTable.RCLC = version

    PUP.highestSeenRCLCVersion = version
end

local function HookWeakAuras()
    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
end

local function HookMRT()
    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
end

function PUP:InitializeAuraUpdater()
    PUP.highestSeenVersionsTable = {
        addOn = PUP.version,
        auras = {}
    }
    playerVersionsTable = {
        addOn = PUP.version,
        auras = {},
        ignores = {},
        raidRenown = {},
        questsUnlocked = {},
        completedQuests = {},
        maxRenownPossible = {}
    }
    UpdateIgnoredPlayers()
    UpdateMRTNoteHash()

    if C_AddOns.IsAddOnLoaded("RCLootCouncil") then
        UpdateRCLCVersion()
    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 C_AddOns.IsAddOnLoaded("WeakAuras") then
        HookWeakAuras()
    end

    if C_AddOns.IsAddOnLoaded("MRT") then
        HookMRT()
    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, UpdateRaidRenown)

    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 not updatePopupWindow then
            updatePopupWindow = PUP:CreatePopupWindowWithButton()

            updatePopupWindow:SetHideOnClickOutside(false)
            updatePopupWindow:SetText(string.format("|cff%sWarnung|r|n|nDein Addon/Aura sind veraltet!",
                PUP.gs.visual.colorStrings.red))
            updatePopupWindow:SetButtonText(string.format("|cff%sOK|r", PUP.gs.visual.colorStrings.green))
            updatePopupWindow:AddCheckButton("Nicht nochmal anzeigen")
            updatePopupWindow:SetButtonOnClick(
                function(dontShowAgain)
                    PUP:SetNotifyOnReadyCheck(not dontShowAgain)

                    updatePopupWindow.checkButton:SetChecked(false)
                end
            )
        end

        if PrepullUpdaterSaved.settings.readyCheckPopup and not PUP.upToDate then
            updatePopupWindow:Pop()
            updatePopupWindow:SetPoint("CENTER", UIParent, "CENTER", 0, 150)
        end

        if WeakAuras.CurrentEncounter then return end
        if PrepullUpdaterSaved.settings.checkCooldowns and PUP:Difficultycheck(false, 15) and UnitInRaid("player") then
            PUP:CheckCooldowns()
        end
    elseif event == "ADDON_LOADED" then
        local name = ...
        if name == "RCLootCouncil" then
            UpdateRCLCVersion()
        elseif name == "WeakAuras" then
            HookWeakAuras()
        elseif name == "MRT" then
            HookMRT()
        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:RegisterEvent("ADDON_LOADED")
f:SetScript("OnEvent", OnEvent)
