local Cowmera = _G.Cowmera or {}
_G.Cowmera = Cowmera

CowmeraDB = CowmeraDB or {}
CowmeraDB.keys = CowmeraDB.keys or {}
CowmeraDB.keys.players = CowmeraDB.keys.players or {}
CowmeraDB.keys.guildPlayers = CowmeraDB.keys.guildPlayers or {}

local PREFIX = "CowmeraKeys"
local KEYS_DB = CowmeraDB.keys
local KEYS_PANEL = nil
local ROWS = {}
local REFRESH_TICKER = nil


local KEYS_DEFERRED_REFRESH = false
local KEYS_COMBAT_FRAME = CreateFrame("Frame")
KEYS_COMBAT_FRAME:RegisterEvent("PLAYER_REGEN_ENABLED")
KEYS_COMBAT_FRAME:SetScript("OnEvent", function(_, event)
    if event == "PLAYER_REGEN_ENABLED" and KEYS_DEFERRED_REFRESH then
        KEYS_DEFERRED_REFRESH = false
        if Cowmera and Cowmera.RefreshKeysPanel then
            Cowmera.RefreshKeysPanel()
        end
    end
end)

local function CanChangeProtectedKeysFrames()
    return not (InCombatLockdown and InCombatLockdown())
end

local function SafeRowShow(frame)
    if not frame then return end
    if not CanChangeProtectedKeysFrames() then
        KEYS_DEFERRED_REFRESH = true
        return
    end
    if frame.Show and (not frame.IsShown or not frame:IsShown()) then
        frame:Show()
    end
end

local function SafeRowHide(frame)
    if not frame then return end
    if not CanChangeProtectedKeysFrames() then
        KEYS_DEFERRED_REFRESH = true
        return
    end
    if frame.Hide and frame.IsShown and frame:IsShown() then
        frame:Hide()
    end
end


local function GetAddonVersion()
    if C_AddOns and C_AddOns.GetAddOnMetadata then
        return C_AddOns.GetAddOnMetadata("Cowmera", "Version") or "0.0.0"
    end
    if GetAddOnMetadata then
        return GetAddOnMetadata("Cowmera", "Version") or "0.0.0"
    end
    return "0.0.0"
end

local function GetRealmSlug()
    local realm = GetRealmName() or "UnknownRealm"
    realm = realm:gsub("%s+", ""):gsub("'", ""):gsub("%-", "")
    return realm
end

local function NormalizeFullName(name, realm)
    if type(name) ~= "string" or name == "" then return nil end
    if name:find("-", 1, true) then return name end
    realm = realm or GetRealmSlug()
    return tostring(name) .. "-" .. tostring(realm)
end

local function GetUnitFullNameSafe(unit)
    local name, realm
    if UnitFullName then
        name, realm = UnitFullName(unit)
    else
        name = UnitName(unit)
    end
    if not name or name == "" then return nil end
    if not realm or realm == "" then realm = GetRealmSlug() end
    return NormalizeFullName(name, realm)
end

local function SafeMapName(mapID)
    if not mapID or mapID == 0 then return "No Keystone" end
    if C_ChallengeMode and C_ChallengeMode.GetMapUIInfo then
        local ok, name = pcall(C_ChallengeMode.GetMapUIInfo, mapID)
        if ok and type(name) == "string" and name ~= "" then
            return name
        end
    end
    return "Map " .. tostring(mapID)
end

local function GetFactionSensitiveBfaSpell(spellHorde, spellAlliance)
    local factionGroup = UnitFactionGroup("player")
    if factionGroup == "Horde" then
        return spellHorde
    elseif factionGroup == "Alliance" then
        return spellAlliance
    end
    return spellHorde or spellAlliance
end

local TELEPORT_SPELL_BY_MAP = {
    ["Algeth'ar Academy"] = 393273,
    ["Mai Caves"] = 1254559,
    ["Magisters' Terrace"] = 1254572,
    ["Nexus Point"] = 1254563,
    ["Pit of Saron"] = 1254555,
    ["Seat of the Triumvirate"] = 1254551,
    ["Spires of Arak"] = 159898,
    ["Skyreach"] = 159898,
    ["Windrunner Spire"] = 1254400,
    ["Ara-Kara"] = 445417,
    ["Cinderbrew Meadery"] = 445440,
    ["City of Threads"] = 445416,
    ["Darkflame Cleft"] = 445441,
    ["The Dawnbreaker"] = 445414,
    ["Eco-Dome Al'dani"] = 1237215,
    ["Operation: Floodgate"] = 1216786,
    ["Priory of the Sacred Flame"] = 445444,
    ["The Rookery"] = 445443,
    ["The Stonevault"] = 445269,
    ["The Azure Vault"] = 393279,
    ["Brackenhide Hollow"] = 393267,
    ["Dawn of the Infinite"] = 424197,
    ["Halls of Infusion"] = 393283,
    ["Neltharus"] = 393276,
    ["The Nokhud Offensive"] = 393262,
    ["Ruby Life Pools"] = 393256,
    ["Uldaman: Legacy of Tyr"] = 393222,
    ["De Other Side"] = 354468,
    ["Halls of Atonement"] = 354465,
    ["Mists of Tirna Scithe"] = 354464,
    ["The Necrotic Wake"] = 354462,
    ["Plaguefall"] = 354463,
    ["Sanguine Depths"] = 354469,
    ["Spires of Ascension"] = 354466,
    ["Tazavesh"] = 367416,
    ["Theatre of Pain"] = 354467,
    ["Atal'Dazar"] = 424187,
    ["Freehold"] = 410071,
    ["Operation: Mechagon"] = 373274,
    ["The MOTHERLODE!!"] = GetFactionSensitiveBfaSpell(467555, 467553),
    ["Siege of Boralus"] = GetFactionSensitiveBfaSpell(464256, 445418),
    ["The Underrot"] = 410074,
    ["Waycrest Manor"] = 424167,
    ["Black Rook Hold"] = 424153,
    ["Court of Stars"] = 393766,
    ["Darkheart Thicket"] = 424163,
    ["Halls of Valor"] = 393764,
    ["Karazhan"] = 373262,
    ["Neltharion's Lair"] = 410078,
    ["Auchindoun"] = 159897,
    ["Bloodmaul Slag Mines"] = 159895,
    ["The Everbloom"] = 159901,
    ["Grimrail Depot"] = 159900,
    ["Iron Docks"] = 159896,
    ["Shadowmoon Burial Grounds"] = 159899,
    ["Upper Blackrock Spire"] = 159902,
    ["Gate of the Setting Sun"] = 131225,
    ["Mogu'shan Palace"] = 131222,
    ["Scholomance"] = 131232,
    ["Scarlet Halls"] = 131231,
    ["Scarlet Monastery"] = 131229,
    ["Siege of Niuzao Temple"] = 131228,
    ["Shado-Pan Monastery"] = 131206,
    ["Stormstout Brewery"] = 131205,
    ["Temple of the Jade Serpent"] = 131204,
    ["Grim Batol"] = 445424,
    ["Throne of the Tides"] = 424142,
    ["The Vortex Pinnacle"] = 410080,
}

local KEY_MAP_ALIASES = {
    ["maisara caverns"] = "Mai Caves",
    ["mai caves"] = "Mai Caves",
    ["nexus point xenas"] = "Nexus Point",
    ["nexus point xena"] = "Nexus Point",
    ["nexus-point xenas"] = "Nexus Point",
    ["nexus-point xena"] = "Nexus Point",
    ["magisters terrace"] = "Magisters' Terrace",
}

local function NormalizeKeyMapName(mapName)
    local text = tostring(mapName or "")
    text = text:gsub("|c%x%x%x%x%x%x%x%x", "")
    text = text:gsub("|r", "")
    text = text:gsub("%s*%b()", "")
    text = text:gsub("^%s+", "")
    text = text:gsub("%s+$", "")
    local simple = text:lower()
    simple = simple:gsub("[^%w%s%-']", " ")
    simple = simple:gsub("%s+", " ")
    simple = simple:gsub("^%s+", "")
    simple = simple:gsub("%s+$", "")
    return text, simple
end

local function SpellKnown(spellID)
    if not spellID then return false end
    if C_SpellBook and C_SpellBook.IsSpellInSpellBook then
        return C_SpellBook.IsSpellInSpellBook(spellID)
    end
    if IsPlayerSpell then
        return IsPlayerSpell(spellID)
    end
    return false
end

local function PlayLockedTeleportSound()
    if PlaySoundFile then
        PlaySoundFile(1537084, "SFX")
    end
end

local function GetTeleportSpellForMapName(mapName)
    local rawText, simple = NormalizeKeyMapName(mapName)
    local canonical = KEY_MAP_ALIASES[simple] or rawText
    local spellID = TELEPORT_SPELL_BY_MAP[canonical]
    if spellID then
        return spellID
    end
    return TELEPORT_SPELL_BY_MAP[tostring(mapName or "")]
end

local function GetOwnedKeyInfo()
    local level, mapID = 0, 0
    if C_MythicPlus and C_MythicPlus.GetOwnedKeystoneLevel then
        local ok, v = pcall(C_MythicPlus.GetOwnedKeystoneLevel)
        if ok then level = tonumber(v) or 0 end
    end
    if C_MythicPlus and C_MythicPlus.GetOwnedKeystoneChallengeMapID then
        local ok, v = pcall(C_MythicPlus.GetOwnedKeystoneChallengeMapID)
        if ok then mapID = tonumber(v) or 0 end
    end
    return level, mapID, SafeMapName(mapID)
end

local function SecondsAgoText(ts)
    local now = time()
    ts = tonumber(ts) or now
    local diff = math.max(0, now - ts)
    if diff < 60 then return tostring(diff) .. "s ago" end
    if diff < 3600 then return tostring(math.floor(diff / 60)) .. "m ago" end
    if diff < 86400 then return tostring(math.floor(diff / 3600)) .. "h ago" end
    return tostring(math.floor(diff / 86400)) .. "d ago"
end

local function EncodeMessage(tbl)
    local parts = {}
    for _, key in ipairs({ "op", "name", "class", "level", "mapID", "mapName", "version", "ts" }) do
        local val = tostring(tbl[key] or "")
        val = val:gsub("|", "/")
        table.insert(parts, val)
    end
    return table.concat(parts, "|")
end

local function DecodeMessage(msg)
    local parts = {}
    for part in tostring(msg or ""):gmatch("([^|]*)") do
        table.insert(parts, part)
        if #parts >= 8 then break end
    end
    return {
        op = parts[1] or "",
        name = parts[2] or "",
        class = parts[3] or "UNKNOWN",
        level = tonumber(parts[4]) or 0,
        mapID = tonumber(parts[5]) or 0,
        mapName = parts[6] or "",
        version = parts[7] or "",
        ts = tonumber(parts[8]) or time(),
    }
end

local function IsInGroupChannel(channel)
    return channel == "PARTY" or channel == "RAID" or channel == "INSTANCE_CHAT"
end

local function SendAddon(channel, payload, target)
    if C_ChatInfo and C_ChatInfo.SendAddonMessage then
        C_ChatInfo.SendAddonMessage(PREFIX, payload, channel, target)
    end
end

local function BuildSelfPayload(op)
    local level, mapID, mapName = GetOwnedKeyInfo()
    local fullName = GetUnitFullNameSafe("player") or NormalizeFullName(UnitName("player") or "Unknown")
    return EncodeMessage({
        op = op or "HELLO",
        name = fullName,
        class = select(2, UnitClass("player")) or "UNKNOWN",
        level = level,
        mapID = mapID,
        mapName = mapName,
        version = GetAddonVersion(),
        ts = time(),
    })
end

local function StoreSeenRecord(bucket, fullName, data)
    if not fullName then return end
    bucket[fullName] = bucket[fullName] or {}
    local rec = bucket[fullName]
    rec.fullName = fullName
    rec.class = data.class or rec.class or "UNKNOWN"
    rec.level = tonumber(data.level) or 0
    rec.mapID = tonumber(data.mapID) or 0
    rec.mapName = (data.mapName and data.mapName ~= "") and data.mapName or SafeMapName(data.mapID)
    rec.version = data.version or rec.version or ""
    rec.updatedAt = tonumber(data.ts) or time()
    rec.hasCowmera = true
end

local function UpdateSelfStored()
    local level, mapID, mapName = GetOwnedKeyInfo()
    local me = GetUnitFullNameSafe("player") or NormalizeFullName(UnitName("player") or "Unknown")
    local data = {
        class = select(2, UnitClass("player")) or "UNKNOWN",
        level = level,
        mapID = mapID,
        mapName = mapName,
        version = GetAddonVersion(),
        ts = time(),
    }
    StoreSeenRecord(KEYS_DB.players, me, data)
    StoreSeenRecord(KEYS_DB.guildPlayers, me, data)
end

local function BroadcastToActiveChannels(op)
    local payload = BuildSelfPayload(op or "HELLO")
    if IsInGroup(LE_PARTY_CATEGORY_INSTANCE) then
        SendAddon("INSTANCE_CHAT", payload)
    elseif IsInRaid() then
        SendAddon("RAID", payload)
    elseif IsInGroup() then
        SendAddon("PARTY", payload)
    end
    if IsInGuild() then
        SendAddon("GUILD", payload)
    end
end

local function RequestScans()
    local payload = BuildSelfPayload("REQ")
    if IsInGroup(LE_PARTY_CATEGORY_INSTANCE) then
        SendAddon("INSTANCE_CHAT", payload)
    elseif IsInRaid() then
        SendAddon("RAID", payload)
    elseif IsInGroup() then
        SendAddon("PARTY", payload)
    end
    if IsInGuild() then
        SendAddon("GUILD", payload)
    end
end

local function RefreshPartyRosterPlaceholders()
    local active = {}
    for _, rec in pairs(KEYS_DB.players) do
        rec.inCurrentGroup = false
    end

    local me = GetUnitFullNameSafe("player")
    if me then
        active[me] = true
        KEYS_DB.players[me] = KEYS_DB.players[me] or { fullName = me }
        KEYS_DB.players[me].inCurrentGroup = true
    end

    if IsInRaid() then
        local count = GetNumGroupMembers() or 0
        for i = 1, count do
            local unit = "raid" .. i
            if UnitExists(unit) then
                local fullName = GetUnitFullNameSafe(unit)
                if fullName then
                    active[fullName] = true
                    KEYS_DB.players[fullName] = KEYS_DB.players[fullName] or { fullName = fullName }
                    KEYS_DB.players[fullName].inCurrentGroup = true
                end
            end
        end
    else
        for i = 1, 4 do
            local unit = "party" .. i
            if UnitExists(unit) then
                local fullName = GetUnitFullNameSafe(unit)
                if fullName then
                    active[fullName] = true
                    KEYS_DB.players[fullName] = KEYS_DB.players[fullName] or { fullName = fullName }
                    KEYS_DB.players[fullName].inCurrentGroup = true
                end
            end
        end
    end

    for name, rec in pairs(KEYS_DB.players) do
        rec.inCurrentGroup = active[name] or false
    end
end

local function SortedGroupRows()
    local rows = {}
    local me = GetUnitFullNameSafe("player")
    for name, rec in pairs(KEYS_DB.players) do
        if rec.inCurrentGroup then
            rec.fullName = name
            table.insert(rows, rec)
        end
    end
    table.sort(rows, function(a, b)
        if a.fullName == me then return true end
        if b.fullName == me then return false end
        return tostring(a.fullName) < tostring(b.fullName)
    end)
    return rows
end

local function SortedGuildRows()
    local rows = {}
    for name, rec in pairs(KEYS_DB.guildPlayers) do
        if (tonumber(rec.level) or 0) > 0 or (tonumber(rec.mapID) or 0) > 0 then
            rec.fullName = name
            table.insert(rows, rec)
        end
    end
    table.sort(rows, function(a, b)
        local al = tonumber(a.level) or 0
        local bl = tonumber(b.level) or 0
        if al ~= bl then return al > bl end
        return tostring(a.fullName) < tostring(b.fullName)
    end)
    return rows
end

local function ShortName(fullName)
    return tostring(fullName or "Unknown"):match("^[^-]+") or tostring(fullName or "Unknown")
end

local function RowText(rec, isGuild)
    local name = ShortName(rec.fullName)
    if not rec.hasCowmera and not isGuild then
        return name, "No Cowmera", "", ""
    end

    local keyText = "No Keystone"
    local level = tonumber(rec.level) or 0
    local mapName = rec.mapName or SafeMapName(rec.mapID)
    if level > 0 and mapName and mapName ~= "" and mapName ~= "No Keystone" then
        keyText = "+" .. tostring(level) .. " " .. tostring(mapName)
    elseif level > 0 then
        keyText = "+" .. tostring(level)
    end

    local version = rec.version and rec.version ~= "" and rec.version or ""
    local age = rec.updatedAt and SecondsAgoText(rec.updatedAt) or ""
    return name, keyText, version, age
end

local function BuildDataRows()
    local dataRows = {}

    table.insert(dataRows, { kind = "header", left = "Party / Instance / Raid", mid = "Keystone", version = "Version", age = "Updated" })
    for _, rec in ipairs(SortedGroupRows()) do
        local left, mid, version, age = RowText(rec, false)
        table.insert(dataRows, { kind = "data", left = left, mid = mid, version = version, age = age, record = rec, isGuild = false })
    end

    table.insert(dataRows, { kind = "spacer" })
    table.insert(dataRows, { kind = "header", left = "Guild (Cowmera + key only)", mid = "Keystone", version = "Version", age = "Updated" })

    local guildRows = SortedGuildRows()
    if #guildRows == 0 then
        table.insert(dataRows, { kind = "text", left = "No guild keystones seen yet", mid = "", version = "", age = "" })
    else
        for _, rec in ipairs(guildRows) do
            local left, mid, version, age = RowText(rec, true)
            table.insert(dataRows, { kind = "data", left = left, mid = mid, version = version, age = age, record = rec, isGuild = true })
        end
    end

    return dataRows
end

function Cowmera.RefreshKeysPanel()
    if not KEYS_PANEL then return end
    if InCombatLockdown and InCombatLockdown() then
        KEYS_DEFERRED_REFRESH = true
        return
    end

    local ok, err = pcall(function()
        UpdateSelfStored()
        RefreshPartyRosterPlaceholders()

        local level, _, mapName = GetOwnedKeyInfo()
        local myText = "Your Keystone: |cffffd200"
        if level > 0 and mapName and mapName ~= "No Keystone" then
            myText = myText .. "+" .. tostring(level) .. " " .. tostring(mapName) .. "|r"
        else
            myText = myText .. "No Keystone|r"
        end
        KEYS_PANEL.myKeyText:SetText(myText)

        local dataRows = BuildDataRows()
        KEYS_PANEL.debugText:SetText("Rows: " .. tostring(#dataRows) .. "  |  Click blue keystones to teleport.")

        for i = 1, #ROWS do
            local row = ROWS[i]
            local data = dataRows[i]

            if not data then
                SafeRowHide(row)
            else
                SafeRowShow(row)

                if not InCombatLockdown() then
                    row.midButton:SetAttribute("type", nil)
                    row.midButton:SetAttribute("type1", nil)
                    row.midButton:SetAttribute("spell", nil)
                    row.midButton:SetAttribute("spell1", nil)
                end
                row.midButton.spellID = nil
                row.midButton.mapName = nil

                if data.kind == "spacer" then
                    row.left:SetText(" ")
                    row.midLabel:SetText(" ")
                    row.version:SetText("")
                    row.age:SetText("")
                elseif data.kind == "header" then
                    row.left:SetText("|cffffd200" .. tostring(data.left or "") .. "|r")
                    row.midLabel:SetText("|cffffd200" .. tostring(data.mid or "") .. "|r")
                    row.version:SetText("|cffffd200" .. tostring(data.version or "") .. "|r")
                    row.age:SetText("|cffffd200" .. tostring(data.age or "") .. "|r")
                else
                    row.left:SetText(tostring(data.left or ""))
                    row.midLabel:SetText(tostring(data.mid or ""))
                    row.version:SetText(tostring(data.version or ""))
                    row.age:SetText(tostring(data.age or ""))

                    local rec = data.record
                    local spellID = rec and rec.hasCowmera and GetTeleportSpellForMapName(rec.mapName) or nil
                    local levelNum = rec and (tonumber(rec.level) or 0) or 0

                    if spellID and levelNum > 0 then
                        row.midLabel:SetText("|cff66ccff" .. tostring(data.mid or "") .. "|r")
                        row.midButton.spellID = spellID
                        row.midButton.mapName = rec.mapName
                        if not InCombatLockdown() then
                            row.midButton:SetAttribute("type", "spell")
                            row.midButton:SetAttribute("type1", "spell")
                            row.midButton:SetAttribute("spell", spellID)
                            row.midButton:SetAttribute("spell1", spellID)
                        end
                    elseif not rec or rec.hasCowmera then
                        row.midLabel:SetText("|cffffffff" .. tostring(data.mid or "") .. "|r")
                    else
                        row.midLabel:SetText("|cffff4040" .. tostring(data.mid or "") .. "|r")
                    end
                end
            end
        end
    end)

    if not ok and KEYS_PANEL and KEYS_PANEL.debugText then
        KEYS_PANEL.debugText:SetText("|cffff4040Keys error:|r " .. tostring(err))
        for i = 1, #ROWS do
            ROWS[i]:Hide()
        end
    end
end

function Cowmera.BuildKeysPanel(host)
    if not host then return end
    if host.__cowmeraKeysBuilt then return end
    host.__cowmeraKeysBuilt = true
    KEYS_PANEL = host

    local title = host:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge")
    title:SetPoint("TOPLEFT", 18, -18)
    title:SetText("Keystones")

    local subtitle = host:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
    subtitle:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -8)
    subtitle:SetWidth(620)
    subtitle:SetJustifyH("LEFT")
    subtitle:SetText("")

    local myKeyText = host:CreateFontString(nil, "ARTWORK", "GameFontNormal")
    myKeyText:SetPoint("TOPLEFT", subtitle, "BOTTOMLEFT", 0, -14)
    myKeyText:SetText("Your Keystone: Loading...")
    host.myKeyText = myKeyText

    local refreshBtn = CreateFrame("Button", nil, host, "UIPanelButtonTemplate")
    refreshBtn:SetSize(120, 24)
    refreshBtn:SetPoint("TOPRIGHT", -18, -18)
    refreshBtn:SetText("Scan Keys")
    refreshBtn:SetScript("OnClick", function()
        UpdateSelfStored()
        RequestScans()
        BroadcastToActiveChannels("HELLO")
        C_Timer.After(1.0, function()
            Cowmera.RefreshKeysPanel()
        end)
    end)

    local holder = CreateFrame("Frame", nil, host, "BackdropTemplate")
    holder:SetPoint("TOPLEFT", 18, -110)
    holder:SetPoint("BOTTOMRIGHT", -18, 18)
    if holder.SetBackdrop then
        holder:SetBackdrop({
            bgFile = "Interface/ChatFrame/ChatFrameBackground",
            edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
            tile = true,
            tileSize = 16,
            edgeSize = 12,
            insets = { left = 3, right = 3, top = 3, bottom = 3 },
        })
        holder:SetBackdropColor(0, 0, 0, 0.35)
        holder:SetBackdropBorderColor(0.65, 0.4, 0.08, 0.9)
    end

    local debugText = holder:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    debugText:SetPoint("TOPLEFT", 12, -10)
    debugText:SetWidth(620)
    debugText:SetJustifyH("LEFT")
    debugText:SetText("Building keys list...")
    host.debugText = debugText

    for i = 1, 24 do
        local row = CreateFrame("Frame", nil, holder)
        row:SetSize(640, 20)
        row:SetPoint("TOPLEFT", 12, -32 - ((i - 1) * 21))

        row.left = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        row.left:SetPoint("LEFT", row, "LEFT", 0, 0)
        row.left:SetWidth(180)
        row.left:SetJustifyH("LEFT")

        row.midButton = CreateFrame("Button", nil, row, "SecureActionButtonTemplate")
        row.midButton:SetPoint("LEFT", row, "LEFT", 190, 0)
        row.midButton:SetSize(250, 18)
        row.midButton:RegisterForClicks("AnyUp", "AnyDown")

        row.midLabel = row.midButton:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        row.midLabel:SetAllPoints(row.midButton)
        row.midLabel:SetJustifyH("LEFT")

        row.version = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        row.version:SetPoint("LEFT", row, "LEFT", 450, 0)
        row.version:SetWidth(80)
        row.version:SetJustifyH("LEFT")

        row.age = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        row.age:SetPoint("LEFT", row, "LEFT", 540, 0)
        row.age:SetWidth(90)
        row.age:SetJustifyH("LEFT")

        row.midButton:SetScript("OnEnter", function(self)
            if GameTooltip and self.mapName and self.mapName ~= "" then
                GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
                GameTooltip:SetText("Teleport to " .. tostring(self.mapName))
                GameTooltip:AddLine("Uses the dungeon teleport portal spell.", 1, 1, 1, true)
                if self.spellID and not SpellKnown(self.spellID) then
                    GameTooltip:AddLine("You do not know this teleport yet.", 1, 0.25, 0.25, true)
                end
                GameTooltip:Show()
            end
        end)
        row.midButton:SetScript("OnLeave", function()
            if GameTooltip then GameTooltip:Hide() end
        end)
        row.midButton:SetScript("OnMouseDown", function(self)
            if self.spellID and not SpellKnown(self.spellID) then
                PlayLockedTeleportSound()
            end
        end)
        ROWS[i] = row
    end

    Cowmera.RefreshKeysPanel()
end

local events = CreateFrame("Frame")
events:RegisterEvent("PLAYER_LOGIN")
events:RegisterEvent("PLAYER_ENTERING_WORLD")
events:RegisterEvent("GROUP_ROSTER_UPDATE")
events:RegisterEvent("CHAT_MSG_ADDON")
events:RegisterEvent("PLAYER_GUILD_UPDATE")
events:RegisterEvent("BAG_UPDATE_DELAYED")
events:RegisterEvent("CHALLENGE_MODE_MAPS_UPDATE")
events:RegisterEvent("CHALLENGE_MODE_COMPLETED")

events:SetScript("OnEvent", function(_, event, ...)
    if event == "PLAYER_LOGIN" then
        if C_ChatInfo and C_ChatInfo.RegisterAddonMessagePrefix then
            C_ChatInfo.RegisterAddonMessagePrefix(PREFIX)
        end
        UpdateSelfStored()
        C_Timer.After(2, function()
            BroadcastToActiveChannels("HELLO")
            RequestScans()
            Cowmera.RefreshKeysPanel()
        end)
        if REFRESH_TICKER then
            REFRESH_TICKER:Cancel()
        end
        REFRESH_TICKER = C_Timer.NewTicker(15, function()
            UpdateSelfStored()
            Cowmera.RefreshKeysPanel()
        end)
    elseif event == "PLAYER_ENTERING_WORLD" or event == "GROUP_ROSTER_UPDATE" or event == "PLAYER_GUILD_UPDATE" then
        UpdateSelfStored()
        RefreshPartyRosterPlaceholders()
        C_Timer.After(1, function()
            BroadcastToActiveChannels("HELLO")
            RequestScans()
            Cowmera.RefreshKeysPanel()
        end)
    elseif event == "BAG_UPDATE_DELAYED" or event == "CHALLENGE_MODE_MAPS_UPDATE" or event == "CHALLENGE_MODE_COMPLETED" then
        UpdateSelfStored()
        BroadcastToActiveChannels("HELLO")
        Cowmera.RefreshKeysPanel()
    elseif event == "CHAT_MSG_ADDON" then
        local prefix, msg, channel, sender = ...
        if prefix ~= PREFIX then return end

        local data = DecodeMessage(msg)
        local fullName = NormalizeFullName(data.name or sender)
        if not fullName then return end

        data.ts = tonumber(data.ts) or time()
        data.mapName = (data.mapName and data.mapName ~= "") and data.mapName or SafeMapName(data.mapID)

        if IsInGroupChannel(channel) then
            StoreSeenRecord(KEYS_DB.players, fullName, data)
        elseif channel == "GUILD" then
            StoreSeenRecord(KEYS_DB.guildPlayers, fullName, data)
        end

        if data.op == "REQ" then
            if IsInGroupChannel(channel) then
                if IsInGroup(LE_PARTY_CATEGORY_INSTANCE) then
                    SendAddon("INSTANCE_CHAT", BuildSelfPayload("HELLO"))
                elseif IsInRaid() then
                    SendAddon("RAID", BuildSelfPayload("HELLO"))
                elseif IsInGroup() then
                    SendAddon("PARTY", BuildSelfPayload("HELLO"))
                end
            elseif channel == "GUILD" then
                SendAddon("GUILD", BuildSelfPayload("HELLO"))
            end
        end

        Cowmera.RefreshKeysPanel()
    end
end)
