----------------------------------------------------------------------
-- MetaAchievementLookup  –  Main.lua
-- /mal lookup <id>   — query the shipped reverse-index
-- /mal gendata       — (dev) scan all achievements, build reverse-index
----------------------------------------------------------------------

local ADDON_NAME = "MetaAchievementLookup"
local PREFIX = "|cff00ccff[MAL]|r "

-- Criteria type constant: "Complete Achievement" (Blizzard: CRITERIA_TYPE_ACHIEVEMENT = 8)
local CRITERIA_TYPE_ACHIEVEMENT = 8

-- Chunk size for the generator scan (achievements per frame)
local SCAN_CHUNK = 200

-- Convenience print
local function Print(msg)
    DEFAULT_CHAT_FRAME:AddMessage(PREFIX .. msg)
end

----------------------------------------------------------------------
-- A) Chain-aware meta lookup (handles progressive achievement chains)
----------------------------------------------------------------------

-- Collect meta IDs for a single achievement from the data table.
local function CollectMetas(data, achID, seen, result)
    local metas = data[achID]
    if not metas then return end
    for _, metaID in ipairs(metas) do
        if not seen[metaID] then
            seen[metaID] = true
            result[#result + 1] = metaID
        end
    end
end

-- Walk the progressive chain (Normal <-> Heroic <-> Mythic) in both
-- directions and collect all meta-achievement IDs that reference any
-- link in the chain.
local function GetMetasForChain(achID)
    local data = MetaAchievementLookup_Data
    if not data then return {} end

    local seen   = {}
    local result = {}

    -- Walk backwards through predecessors (including achID itself)
    local current = achID
    while current and current > 0 do
        CollectMetas(data, current, seen, result)
        local prev = GetPreviousAchievement(current)
        if not prev or prev == current then break end
        current = prev
    end

    -- Walk forwards through successors
    current = achID
    while current and current > 0 do
        local nxt = GetNextAchievement(current)
        if not nxt or nxt == current then break end
        CollectMetas(data, nxt, seen, result)
        current = nxt
    end

    return result
end

----------------------------------------------------------------------
-- B) Runtime lookup  (/mal lookup <id>)
----------------------------------------------------------------------

local function DoLookup(input)
    local idStr = strtrim(input or "")
    local childID = tonumber(idStr)
    if not childID then
        Print("Usage: /mal lookup <achievementID>")
        return
    end

    -- Validate the child achievement exists
    local _, childName = GetAchievementInfo(childID)
    if not childName then
        Print(format("Achievement %d does not exist or is not available.", childID))
        return
    end
    Print(format("Meta-achievements containing |cffffff00[%d] %s|r:", childID, childName))

    local metas = GetMetasForChain(childID)
    if #metas == 0 then
        Print("  (none found in shipped data)")
        return
    end

    -- Build sortable list with names
    local list = {}
    for _, metaID in ipairs(metas) do
        local _, metaName = GetAchievementInfo(metaID)
        list[#list + 1] = { id = metaID, name = metaName or "(unknown)" }
    end
    table.sort(list, function(a, b) return a.id < b.id end)

    for _, entry in ipairs(list) do
        Print(format("  %d — %s", entry.id, entry.name))
    end
    Print(format("Total: %d meta-achievement(s).", #list))
end

----------------------------------------------------------------------
-- C) Dev generator  (/mal gendata — chunked scan via C_Timer)
----------------------------------------------------------------------

local FinishGenData  -- forward declaration

local function CollectAllAchievementIDs()
    local ids = {}
    local seen = {}

    -- Walk every achievement category
    local categories = GetCategoryList() or {}
    for _, catID in ipairs(categories) do
        local numAch = GetCategoryNumAchievements(catID, true) or 0  -- true = includeAll
        for idx = 1, numAch do
            local achID = GetAchievementInfo(catID, idx)
            if achID and not seen[achID] then
                seen[achID] = true
                ids[#ids + 1] = achID
            end
        end
    end

    -- Also walk guild categories
    local guildCats = GetGuildCategoryList() or {}
    for _, catID in ipairs(guildCats) do
        local numAch = GetCategoryNumAchievements(catID, true) or 0
        for idx = 1, numAch do
            local achID = GetAchievementInfo(catID, idx)
            if achID and not seen[achID] then
                seen[achID] = true
                ids[#ids + 1] = achID
            end
        end
    end

    table.sort(ids)
    return ids
end

local function DoGenData()
    Print("Starting achievement scan...")
    local allIDs = CollectAllAchievementIDs()
    local total = #allIDs
    if total == 0 then
        Print("No achievements found. Are you in-game?")
        return
    end
    Print(format("Found %d unique achievements to scan.", total))

    local reverseMap = {}   -- childID -> { metaID, ... }
    local metaCount = 0     -- number of distinct meta-achievements found
    local cursor = 1

    local ticker
    ticker = C_Timer.NewTicker(0, function()
        local batchEnd = min(cursor + SCAN_CHUNK - 1, total)
        for i = cursor, batchEnd do
            local achID = allIDs[i]
            local numCriteria = GetAchievementNumCriteria(achID) or 0
            local isMeta = false
            for ci = 1, numCriteria do
                local ok, _, criteriaType, _,
                      _, _, _, _, assetID =
                      pcall(GetAchievementCriteriaInfo, achID, ci)
                if ok and criteriaType == CRITERIA_TYPE_ACHIEVEMENT and assetID and assetID > 0 then
                    if not reverseMap[assetID] then
                        reverseMap[assetID] = {}
                    end
                    reverseMap[assetID][#reverseMap[assetID] + 1] = achID
                    isMeta = true
                end
            end
            if isMeta then metaCount = metaCount + 1 end
        end

        -- Progress
        if batchEnd % 500 < SCAN_CHUNK or batchEnd == total then
            Print(format("  Scanned %d / %d achievements...", batchEnd, total))
        end

        cursor = batchEnd + 1
        if cursor > total then
            ticker:Cancel()
            FinishGenData(reverseMap, metaCount, total)
        end
    end)
end

----------------------------------------------------------------------
-- Output window (scrollable EditBox + Select All / Close buttons)
----------------------------------------------------------------------

local outputFrame

local function CreateOutputFrame()
    if outputFrame then return outputFrame end

    local f = CreateFrame("Frame", "MAL_OutputFrame", UIParent, "BackdropTemplate")
    f:SetSize(700, 500)
    f:SetPoint("CENTER")
    f:SetBackdrop({
        bgFile   = "Interface\\DialogFrame\\UI-DialogBox-Background",
        edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
        tile = true, tileSize = 32, edgeSize = 32,
        insets = { left = 8, right = 8, top = 8, bottom = 8 },
    })
    f:SetBackdropColor(0, 0, 0, 0.9)
    f:SetMovable(true)
    f:EnableMouse(true)
    f:RegisterForDrag("LeftButton")
    f:SetScript("OnDragStart", f.StartMoving)
    f:SetScript("OnDragStop", f.StopMovingOrSizing)
    f:SetFrameStrata("DIALOG")
    tinsert(UISpecialFrames, "MAL_OutputFrame")

    -- Title
    local title = f:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    title:SetPoint("TOP", 0, -14)
    title:SetText("MetaAchievementLookup — Generated Data")

    -- Scroll frame
    local sf = CreateFrame("ScrollFrame", "MAL_ScrollFrame", f, "UIPanelScrollFrameTemplate")
    sf:SetPoint("TOPLEFT", 16, -40)
    sf:SetPoint("BOTTOMRIGHT", -36, 50)

    -- EditBox inside scroll frame
    local eb = CreateFrame("EditBox", "MAL_EditBox", sf)
    eb:SetMultiLine(true)
    eb:SetAutoFocus(false)
    eb:SetFontObject(GameFontHighlightSmall)
    eb:SetWidth(sf:GetWidth() or 640)
    eb:SetScript("OnEscapePressed", eb.ClearFocus)
    sf:SetScrollChild(eb)

    -- "Select All" button (user then Ctrl+C)
    local selBtn = CreateFrame("Button", nil, f, "UIPanelButtonTemplate")
    selBtn:SetSize(120, 26)
    selBtn:SetPoint("BOTTOMLEFT", 16, 14)
    selBtn:SetText("Select All")
    selBtn:SetScript("OnClick", function()
        eb:SetFocus()
        eb:HighlightText()
    end)

    -- Close button
    local closeBtn = CreateFrame("Button", nil, f, "UIPanelButtonTemplate")
    closeBtn:SetSize(80, 26)
    closeBtn:SetPoint("BOTTOMRIGHT", -16, 14)
    closeBtn:SetText("Close")
    closeBtn:SetScript("OnClick", function() f:Hide() end)

    f.editBox = eb
    outputFrame = f
    return f
end

local function ShowOutputWindow(text)
    local f = CreateOutputFrame()
    f.editBox:SetText(text)
    f.editBox:SetCursorPosition(0)
    f:Show()
end

----------------------------------------------------------------------
-- Finish generation — format output and save to SavedVariables
----------------------------------------------------------------------

FinishGenData = function(reverseMap, metaCount, totalScanned)
    -- Count children
    local childCount = 0
    for _ in pairs(reverseMap) do childCount = childCount + 1 end

    Print(format("Scan complete.  %d achievements scanned.", totalScanned))
    Print(format("  %d meta-achievements found, mapping %d child achievements.", metaCount, childCount))

    -- Store in SavedVariables so user can extract it
    MetaAchievementLookup_GenDB = reverseMap

    -- Build the full Lua snippet
    local lines = {}
    lines[#lines + 1] = "-- Generated by /mal gendata"
    lines[#lines + 1] = "-- Paste this into Data.lua, replacing the existing table.\n"
    lines[#lines + 1] = "MetaAchievementLookup_Data = {"

    -- Sort child IDs for deterministic output
    local sortedChildren = {}
    for childID in pairs(reverseMap) do
        sortedChildren[#sortedChildren + 1] = childID
    end
    table.sort(sortedChildren)

    for _, childID in ipairs(sortedChildren) do
        local metas = reverseMap[childID]
        table.sort(metas)
        local metaStrs = {}
        for _, m in ipairs(metas) do
            metaStrs[#metaStrs + 1] = tostring(m)
        end
        lines[#lines + 1] = format("  [%d] = { %s },", childID, table.concat(metaStrs, ", "))
    end

    lines[#lines + 1] = "}"

    local snippet = table.concat(lines, "\n")
    ShowOutputWindow(snippet)

    Print("Data also saved to SavedVariables (MetaAchievementLookup_GenDB).")
    Print("Use Select All + Ctrl-C in the window, or copy from"
        .. " WTF/<account>/SavedVariables/" .. ADDON_NAME .. ".lua after logout.")
end

----------------------------------------------------------------------
-- Slash command router
----------------------------------------------------------------------

SLASH_MAL1 = "/mal"
SlashCmdList["MAL"] = function(msg)
    local cmd, rest = strsplit(" ", strtrim(msg), 2)
    cmd = strlower(cmd or "")

    if cmd == "lookup" then
        DoLookup(rest)
    elseif cmd == "gendata" then
        DoGenData()
    else
        Print("Commands:")
        Print("  /mal lookup <achievementID>  — show meta-achievements containing this child")
        Print("  /mal gendata                 — (dev) scan & generate reverse-index data")
    end
end

Print("Loaded. Type /mal for help.")

----------------------------------------------------------------------
-- D) Achievement frame integration  — "Required for" button + inline mini cards
--
--    Strategy: expand the achievement button's height so the ScrollBox
--    allocates room (no overlap). Pin the original textures to their
--    natural height so they don't stretch. Place the mini-card container
--    in the extra space below. Track by achievement ID so it survives
--    button-frame recycling on scroll.
----------------------------------------------------------------------

local MINI_ACH_HEIGHT = 50
local PADDING         = 4
local metaRowPool     = {}
local metaContainer   = nil
local activeButton    = nil
local activeAchID     = nil
local containerVisible = false
local cachedContainerH = 0       -- container height for the active achievement
local inLayoutUpdate   = false   -- guard against recursive FullUpdate

local function CreateMetaContainer()
    if metaContainer then return metaContainer end

    local c = CreateFrame("Frame", "MAL_MetaContainer", UIParent)
    c:Hide()

    local bg = c:CreateTexture(nil, "BACKGROUND")
    bg:SetAllPoints()
    bg:SetColorTexture(0.06, 0.06, 0.06, 0.92)

    local topLine = c:CreateTexture(nil, "BORDER")
    topLine:SetHeight(1)
    topLine:SetPoint("TOPLEFT", 0, 0)
    topLine:SetPoint("TOPRIGHT", 0, 0)
    topLine:SetColorTexture(0.35, 0.35, 0.35, 0.8)

    local botLine = c:CreateTexture(nil, "BORDER")
    botLine:SetHeight(1)
    botLine:SetPoint("BOTTOMLEFT", 0, 0)
    botLine:SetPoint("BOTTOMRIGHT", 0, 0)
    botLine:SetColorTexture(0.35, 0.35, 0.35, 0.8)

    metaContainer = c
    return c
end

local function GetMiniAchRow(index)
    if metaRowPool[index] then return metaRowPool[index] end

    local c = CreateMetaContainer()
    local row = CreateFrame("Button", nil, c, "BackdropTemplate")
    row:SetHeight(MINI_ACH_HEIGHT)
    row:SetBackdrop({
        bgFile   = "Interface\\AchievementFrame\\UI-Achievement-Parchment-Horizontal",
        edgeFile = "Interface\\AchievementFrame\\UI-Achievement-WoodBorder",
        tile = false, edgeSize = 8,
        insets = { left = 4, right = 4, top = 4, bottom = 4 },
    })

    local iconFrame = CreateFrame("Frame", nil, row)
    iconFrame:SetSize(36, 36)
    iconFrame:SetPoint("LEFT", 8, 0)
    row.iconFrame = iconFrame

    local icon = iconFrame:CreateTexture(nil, "ARTWORK")
    icon:SetAllPoints()
    icon:SetTexCoord(0.05, 0.95, 0.05, 0.95)
    row.icon = icon

    local iconBorder = iconFrame:CreateTexture(nil, "OVERLAY")
    iconBorder:SetPoint("TOPLEFT", icon, "TOPLEFT", -3, 3)
    iconBorder:SetPoint("BOTTOMRIGHT", icon, "BOTTOMRIGHT", 3, -3)
    iconBorder:SetTexture("Interface\\AchievementFrame\\UI-Achievement-IconFrame")
    iconBorder:SetTexCoord(0, 0.5625, 0, 0.5625)
    row.iconBorder = iconBorder

    local check = iconFrame:CreateTexture(nil, "OVERLAY", nil, 2)
    check:SetSize(18, 18)
    check:SetPoint("BOTTOMLEFT", icon, "BOTTOMLEFT", -4, -4)
    check:SetTexture("Interface\\RaidFrame\\ReadyCheck-Ready")
    check:Hide()
    row.check = check

    local name = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    name:SetJustifyH("LEFT")
    name:SetPoint("TOPLEFT", iconFrame, "TOPRIGHT", 8, -2)
    name:SetPoint("RIGHT", row, "RIGHT", -8, 0)
    name:SetWordWrap(false)
    row.name = name

    local desc = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    desc:SetJustifyH("LEFT")
    desc:SetJustifyV("TOP")
    desc:SetPoint("TOPLEFT", name, "BOTTOMLEFT", 0, -2)
    desc:SetPoint("RIGHT", row, "RIGHT", -8, 0)
    desc:SetWordWrap(true)
    desc:SetMaxLines(2)
    desc:SetTextColor(0.6, 0.6, 0.6)
    row.desc = desc

    local points = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    points:SetPoint("TOPRIGHT", row, "TOPRIGHT", -10, -6)
    row.points = points

    row:SetHighlightTexture("Interface\\AchievementFrame\\UI-Achievement-Alert-Glow", "ADD")
    row:GetHighlightTexture():SetAlpha(0.15)

    row:SetScript("OnClick", function(self)
        local id = self.achievementID
        if id then
            -- Don't call HideInlineMetaRows() here — hiding the parent
            -- mid-click aborts this handler. The Init hook auto-cleans
            -- when the new achievement appears.
            OpenAchievementFrameToAchievement(id)
        end
    end)
    row:SetScript("OnEnter", function(self)
        if self.achievementID then
            GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
            GameTooltip:SetAchievementByID(self.achievementID)
            GameTooltip:Show()
        end
    end)
    row:SetScript("OnLeave", GameTooltip_Hide)

    metaRowPool[index] = row
    return row
end

local function PopulateContainer(list)
    for i = 1, #list do
        local row = GetMiniAchRow(i)
        local entry = list[i]
        row.achievementID = entry.id
        row.icon:SetTexture(entry.icon)
        row.name:SetText(entry.name)
        row.desc:SetText(entry.description)
        row.points:SetText(entry.pts > 0 and tostring(entry.pts) or "")

        if entry.completed then
            row.icon:SetDesaturated(false)
            row.check:Show()
            row.name:SetTextColor(1, 1, 1)
            row:SetBackdropColor(1, 1, 1, 1)
            row.points:SetTextColor(1, 0.82, 0)
        else
            row.icon:SetDesaturated(true)
            row.check:Hide()
            row.name:SetTextColor(0.6, 0.6, 0.6)
            row:SetBackdropColor(0.5, 0.5, 0.5, 1)
            row.points:SetTextColor(0.5, 0.5, 0.5)
        end

        row:ClearAllPoints()
        row:SetPoint("TOPLEFT", metaContainer, "TOPLEFT", 4, -(PADDING + (i - 1) * (MINI_ACH_HEIGHT + PADDING)))
        row:SetPoint("TOPRIGHT", metaContainer, "TOPRIGHT", -4, -(PADDING + (i - 1) * (MINI_ACH_HEIGHT + PADDING)))
        row:Show()
    end
    for i = #list + 1, #metaRowPool do
        metaRowPool[i]:Hide()
    end
end

local function RefreshScrollBox()
    if inLayoutUpdate then return end
    inLayoutUpdate = true
    if AchievementFrameAchievements and AchievementFrameAchievements.ScrollBox then
        AchievementFrameAchievements.ScrollBox:FullUpdate(ScrollBoxConstants.UpdateImmediately)
    end
    inLayoutUpdate = false
end

-- Pin the achievement button's background/glow textures to the original
-- height so expanding the button doesn't stretch them.
local TEXTURES_TO_PIN = { "Background", "Glow", "Highlight" }

local function PinTextures(button, origH)
    for _, key in ipairs(TEXTURES_TO_PIN) do
        local tex = button[key]
        if tex and tex.SetHeight then
            if not button["MAL_Orig_" .. key] then
                button["MAL_Orig_" .. key] = true  -- mark that we changed it
            end
            tex:ClearAllPoints()
            tex:SetPoint("TOPLEFT", button, "TOPLEFT", 0, 0)
            tex:SetPoint("TOPRIGHT", button, "TOPRIGHT", 0, 0)
            tex:SetHeight(origH)
        end
    end
end

local function UnpinTextures(button)
    for _, key in ipairs(TEXTURES_TO_PIN) do
        if button["MAL_Orig_" .. key] then
            local tex = button[key]
            if tex then
                tex:ClearAllPoints()
                tex:SetAllPoints(button)
            end
            button["MAL_Orig_" .. key] = nil
        end
    end
end

-- Expand a button and attach the container inside its extra space.
local function AttachToButton(button)
    local origH = button.MAL_OrigHeight
    if not origH then return end

    local c = metaContainer
    if not c then return end

    -- Total height: original content + container
    button:SetHeight(origH + cachedContainerH + 2)

    -- Pin original textures to their natural height
    PinTextures(button, origH)

    -- Parent container to button, place in extra space below content
    c:SetParent(button)
    c:SetFrameLevel(button:GetFrameLevel() + 10)
    c:ClearAllPoints()
    c:SetPoint("TOPLEFT", button, "TOPLEFT", 0, -(origH + 1))
    c:SetPoint("TOPRIGHT", button, "TOPRIGHT", 0, -(origH + 1))
    c:SetHeight(cachedContainerH)
    c:Show()

    activeButton = button
end

local function RestoreButton(button)
    if not button then return end
    if button.MAL_OrigHeight then
        -- Only restore height if the button isn't collapsed; Blizzard's
        -- Collapse already set the correct collapsed height.
        if not button.collapsed then
            button:SetHeight(button.MAL_OrigHeight)
        end
        button.MAL_OrigHeight = nil
    end
    UnpinTextures(button)
end

local function ShowInlineMetaRows(button, achID)
    local metas = GetMetasForChain(achID)
    if #metas == 0 then return end

    local list = {}
    for _, metaID in ipairs(metas) do
        local _, metaName, metaPoints, metaCompleted, _, _, _, metaDesc, _, metaIcon = GetAchievementInfo(metaID)
        list[#list + 1] = {
            id          = metaID,
            name        = metaName or ("Achievement #" .. metaID),
            description = metaDesc or "",
            completed   = metaCompleted,
            icon        = metaIcon or 136243,
            pts         = metaPoints or 0,
        }
    end
    table.sort(list, function(a, b) return a.name < b.name end)

    CreateMetaContainer()
    cachedContainerH = PADDING + #list * (MINI_ACH_HEIGHT + PADDING)

    PopulateContainer(list)

    -- Save the button's natural height before expanding
    if not button.MAL_OrigHeight then
        button.MAL_OrigHeight = button:GetHeight()
    end

    AttachToButton(button)

    activeAchID = achID
    containerVisible = true
    RefreshScrollBox()
end

local function HideInlineMetaRows()
    if metaContainer then metaContainer:Hide() end
    RestoreButton(activeButton)
    activeButton = nil
    activeAchID = nil
    containerVisible = false
    cachedContainerH = 0
    RefreshScrollBox()
end

local function HookAchievementUI()
    if not AchievementTemplateMixin then return end

    -- Hide cards when a different achievement is selected (clicked/expanded).
    -- This is safer than detecting it in Init, which fires for every recycled
    -- button during scroll and would falsely dismiss the container.
    if AchievementTemplateMixin.Expand then
        hooksecurefunc(AchievementTemplateMixin, "Expand", function(button)
            if containerVisible and activeAchID and activeAchID ~= button.id then
                HideInlineMetaRows()
            end
        end)
    end

    hooksecurefunc(AchievementTemplateMixin, "Init", function(button, elementData)
        local achID = button.id

        -- If this recycled button previously held our expanded rows
        -- for a different achievement, restore it
        if button.MAL_OrigHeight and not (containerVisible and activeAchID == achID) then
            RestoreButton(button)
            if activeButton == button then
                activeButton = nil
            end
        end

        -- Create the "Required for" button once per frame
        if not button.MAL_RequiredForBtn then
            local btn = CreateFrame("Button", nil, button, "UIPanelButtonTemplate")
            btn:SetSize(80, 18)
            btn:SetText("Required for")
            btn:GetFontString():SetFont(btn:GetFontString():GetFont(), 10)
            -- Raise frame level above the Track checkbox so clicks register
            btn:SetFrameLevel(button:GetFrameLevel() + 5)
            btn:SetScript("OnClick", function(self)
                local parent = self:GetParent()
                local wasShowing = containerVisible and activeAchID == parent.id
                HideInlineMetaRows()
                if not wasShowing then
                    ShowInlineMetaRows(parent, parent.id)
                end
            end)
            button.MAL_RequiredForBtn = btn
        end

        -- Place at bottom-left of the achievement content
        local naturalH = button.MAL_OrigHeight or button:GetHeight()
        button.MAL_RequiredForBtn:ClearAllPoints()
        button.MAL_RequiredForBtn:SetPoint("TOPLEFT", button, "TOPLEFT", 56, -(naturalH - 5 - 18))
        button.MAL_RequiredForBtn:SetFrameLevel(button:GetFrameLevel() + 5)

        local hasMetas = #GetMetasForChain(achID) > 0
        button.MAL_RequiredForBtn:SetShown(not button.collapsed and hasMetas)

        -- Re-attach container when ScrollBox recycles a button for our active achievement
        if containerVisible and activeAchID == achID and not button.collapsed then
            if not button.MAL_OrigHeight then
                button.MAL_OrigHeight = button:GetHeight()
            end
            -- Detach from old button if it was a different frame
            if activeButton and activeButton ~= button then
                RestoreButton(activeButton)
            end
            AttachToButton(button)
        end
    end)

    if AchievementTemplateMixin.Collapse then
        hooksecurefunc(AchievementTemplateMixin, "Collapse", function(button)
            if activeAchID == button.id then
                HideInlineMetaRows()
            end
        end)
    end
end

-- Wait for AchievementFrame to load (it's load-on-demand)
local loader = CreateFrame("Frame")
loader:RegisterEvent("ADDON_LOADED")
loader:SetScript("OnEvent", function(self, _, addon)
    if addon == "Blizzard_AchievementUI" then
        HookAchievementUI()
        self:UnregisterEvent("ADDON_LOADED")
    end
end)
if AchievementTemplateMixin then
    HookAchievementUI()
end
