Jump to content

Module:AchievementSystem

Revision as of 20:13, 1 April 2025 by MarkWD (talk | contribs) (// via Wikitext Extension for VSCode)

Documentation for this module may be created at Module:AchievementSystem/doc

-- Module:AchievementSystem
-- Achievement system that loads data from MediaWiki:AchievementData.json.
-- STYLING NOTE: All achievement styling is defined in CSS/Templates.css, not in the JSON.
-- This module only assigns CSS classes based on achievement IDs in the format:
--   .person-template .template-title.achievement-{id}::after {}
--
-- The module does not use any styling information from the JSON data structure.

local Achievements = {}

-- Debug configuration
local DEBUG_MODE = true
local function debugLog(message)
    if not DEBUG_MODE then return end
    pcall(function()
        mw.logObject({
            system = "achievement_simple",
            message = message,
            timestamp = os.date('%H:%M:%S')
        })
    end)
    mw.log("ACHIEVEMENT-DEBUG: " .. message)
end

--------------------------------------------------------------------------------
-- JSON Handling
--------------------------------------------------------------------------------
-- We'll use MediaWiki's built-in JSON functions directly, no external module needed
local function jsonDecode(jsonString)
    if not jsonString then 
        debugLog('ERROR: Empty JSON string provided')
        return nil 
    end
    
    if mw.text and mw.text.jsonDecode then
        local success, result = pcall(function()
            return mw.text.jsonDecode(jsonString)
        end)
        
        if success and result then
            -- Validate the structure has achievement_types array
            if type(result) == 'table' and result.achievement_types then
                debugLog('SUCCESS: JSON decode successful with ' .. #result.achievement_types .. ' achievement types')
                return result
            else
                debugLog('ERROR: JSON decode succeeded but missing achievement_types array or not a table')
                if type(result) == 'table' then
                    for k, _ in pairs(result) do
                        debugLog('Found key in result: ' .. k)
                    end
                end
            end
        else
            debugLog('ERROR: JSON decode failed: ' .. tostring(result or 'unknown error'))
        end
    end
    
    debugLog('CRITICAL ERROR: mw.text.jsonDecode not available!')
    return nil
end

-- Simple HTML encode fallback
local function htmlEncode(str)
    if mw.text and mw.text.htmlEncode then
        return mw.text.htmlEncode(str or '')
    else
        return (str or '')
            :gsub('&', '&')
            :gsub('<', '&lt;')
            :gsub('>', '&gt;')
            :gsub('"', '&quot;')
    end
end

--------------------------------------------------------------------------------
-- Configuration, Default Data, and Cache
--------------------------------------------------------------------------------

local ACHIEVEMENT_DATA_PAGE = 'MediaWiki:AchievementData.json'
local dataCache = nil

local DEFAULT_DATA = {
    schema_version = 1,
    last_updated = os.date('!%Y-%m-%dT%H:%M:%SZ'),
    achievement_types = {},
    user_achievements = {},
    cache_control = { version = 0 }
}

--------------------------------------------------------------------------------
-- Configuration
--------------------------------------------------------------------------------
-- This array maps legacy achievement IDs to standardized ones
local ACHIEVEMENT_TYPE_MAPPING = {
    ["title-test"] = "dev-role",
    ["jedi"]      = "ach1",
    ["champion"]  = "ach2",
    ["sponsor"]   = "ach3"
}

-- Normalizes achievement type to handle variants or legacy types
local function normalizeAchievementType(achievementType)
    if not achievementType then 
        debugLog("Normalize called with nil achievement type")
        return nil 
    end
    
    -- Always log the normalization attempt for debugging
    debugLog("Normalizing achievement type: '" .. tostring(achievementType) .. "'")
    
    -- If it's already a standard type, return it directly
    if achievementType == "dev-role" or 
       achievementType == "ach1" or 
       achievementType == "ach2" or 
       achievementType == "ach3" or
       achievementType == "sponsor-role" then
        debugLog("Achievement type already standardized: " .. achievementType)
        return achievementType
    end
    
    -- Otherwise check the mapping table
    local mappedType = ACHIEVEMENT_TYPE_MAPPING[achievementType] or achievementType
    
    if mappedType ~= achievementType then
        debugLog("Mapped legacy achievement type '" .. achievementType .. "' to '" .. mappedType .. "'")
    end
    
    return mappedType
end

--------------------------------------------------------------------------------
-- Load achievement data from the JSON page
--------------------------------------------------------------------------------
function Achievements.loadData()
    mw.log("JSON-DEBUG: Starting to load achievement data")

    -- Use the request-level cache if we already loaded data once
    if dataCache then
        mw.log("JSON-DEBUG: Using request-level cached data")
        return dataCache
    end

    local data = DEFAULT_DATA
    local jsonLoadingMethod = "none"
    
    -- SIMPLIFIED LOADING APPROACH - prioritizing mw.text.jsonDecode as it correctly processes achievement types
    mw.log("JSON-DEBUG: === SIMPLIFIED JSON LOADING START ===")
    
    -- First get the title and check if page exists
    local pageTitle = mw.title.new(ACHIEVEMENT_DATA_PAGE)
    if not pageTitle or not pageTitle.exists then
        mw.log("JSON-DEBUG: " .. ACHIEVEMENT_DATA_PAGE .. " does not exist or title creation failed")
        return DEFAULT_DATA
    end
    
    mw.log("JSON-DEBUG: Page exists with content model: " .. tostring(pageTitle.contentModel))
    
    -- First attempt: Use direct raw content with mw.text.jsonDecode
    -- Based on diagnostic output, this is the most reliable method
    if mw.text and mw.text.jsonDecode then
        local content = nil
        local contentSuccess, contentResult = pcall(function()
            return pageTitle:getContent()
        end)
        
        if contentSuccess and contentResult and contentResult ~= "" then
            content = contentResult
            mw.log("JSON-DEBUG: Successfully retrieved raw content, length: " .. #content)
            
            -- Remove any BOM or leading whitespace that might cause issues
            content = content:gsub("^%s+", "")
            if content:byte(1) == 239 and content:byte(2) == 187 and content:byte(3) == 191 then
                mw.log("JSON-DEBUG: Removing UTF-8 BOM from content")
                content = content:sub(4)
            end
            
            local jsonDecodeSuccess, jsonData = pcall(function()
                return mw.text.jsonDecode(content)
            end)
            
            if jsonDecodeSuccess and jsonData and type(jsonData) == 'table' then
                -- Validate the structure
                if jsonData.achievement_types and #jsonData.achievement_types > 0 and jsonData.user_achievements then
                    mw.log("JSON-DEBUG: ✓ Successfully decoded with mw.text.jsonDecode, found " .. 
                          #jsonData.achievement_types .. " achievement types")
                    
                    -- Log achievement types
                    for i, typeData in ipairs(jsonData.achievement_types) do
                        mw.log("JSON-DEBUG: Type[" .. i .. "]: id=" .. (typeData.id or "nil") .. 
                               ", name=" .. (typeData.name or "nil") .. 
                               ", type=" .. (typeData.type or "nil"))
                    end
                    
                    data = jsonData
                    jsonLoadingMethod = "mw.text.jsonDecode"
                else
                    mw.log("JSON-DEBUG: ✗ mw.text.jsonDecode result missing required fields or empty achievement_types")
                    if jsonData.achievement_types then
                        mw.log("JSON-DEBUG: Found achievement_types but it contains " .. 
                              #jsonData.achievement_types .. " items")
                    end
                end
            else
                mw.log("JSON-DEBUG: ✗ mw.text.jsonDecode failed: " .. 
                      tostring(jsonData or 'unknown error'))
            end
        else
            mw.log("JSON-DEBUG: ✗ Failed to get raw content: " .. 
                  tostring(contentResult or 'unknown error'))
        end
    else
        mw.log("JSON-DEBUG: ✗ mw.text.jsonDecode not available")
    end
    
    -- Second attempt: Only if first attempt failed, try mw.loadJsonData
    if jsonLoadingMethod == "none" and mw.loadJsonData then
        mw.log("JSON-DEBUG: Attempting mw.loadJsonData as fallback")
        
        local loadJsonSuccess, jsonData = pcall(function()
            return mw.loadJsonData(ACHIEVEMENT_DATA_PAGE)
        end)
        
        if loadJsonSuccess and jsonData and type(jsonData) == 'table' then
            -- Validate the structure
            if jsonData.achievement_types and #jsonData.achievement_types > 0 and jsonData.user_achievements then
                mw.log("JSON-DEBUG: ✓ Successfully loaded with mw.loadJsonData, found " .. 
                      #jsonData.achievement_types .. " achievement types")
                
                data = jsonData
                jsonLoadingMethod = "mw.loadJsonData"
            else
                mw.log("JSON-DEBUG: ✗ mw.loadJsonData result missing required fields or empty achievement_types")
                if jsonData.achievement_types then
                    mw.log("JSON-DEBUG: Found achievement_types but it contains " .. 
                          #jsonData.achievement_types .. " items")
                    
                    -- This is likely the issue identified in diagnostics - achievement_types exists but is empty
                    -- We'll use the data anyway and log the issue
                    if jsonData.user_achievements then
                        mw.log("JSON-DEBUG: Using partial data from mw.loadJsonData despite empty achievement_types")
                        data = jsonData
                        jsonLoadingMethod = "mw.loadJsonData (partial)"
                    end
                end
            end
        else
            mw.log("JSON-DEBUG: ✗ mw.loadJsonData failed: " .. 
                  tostring(jsonData or 'unknown error'))
        end
    end
    
    -- Log which method we used and validation
    mw.log("JSON-DEBUG: JSON loading complete using method: " .. jsonLoadingMethod)
    if data ~= DEFAULT_DATA then
        local achievementTypeCount = data.achievement_types and #data.achievement_types or 0
        local userCount = 0
        if data.user_achievements then
            for _, _ in pairs(data.user_achievements) do
                userCount = userCount + 1
            end
        end
        
        mw.log("JSON-DEBUG: Loaded data with " .. achievementTypeCount .. 
              " achievement types and " .. userCount .. " users")
        
        -- Validate achievement types have required fields
        if data.achievement_types then
            for i, typeData in ipairs(data.achievement_types) do
                if not typeData.id or not typeData.name or not typeData.type then
                    mw.log("JSON-DEBUG: WARNING: Achievement type " .. i .. 
                          " missing required fields (id, name, or type)")
                end
            end
        end
    else
        mw.log("JSON-DEBUG: WARNING: Using default data due to loading failures")
    end
    
    mw.log("JSON-DEBUG: === SIMPLIFIED JSON LOADING END ===")
    
    dataCache = data
    return data
end

--------------------------------------------------------------------------------
-- Check if a page/user has any achievements
--------------------------------------------------------------------------------
function Achievements.hasAchievements(pageId)
    if not pageId or pageId == '' then
        return false
    end

    local data = Achievements.loadData()
    if not data or not data.user_achievements then
        return false
    end

    local key = tostring(pageId)
    if data.user_achievements[key] and #data.user_achievements[key] > 0 then
        return true
    end

    -- Check for legacy "n123" style
    if key:match("^%d+$") then
        local alt = "n" .. key
        if data.user_achievements[alt] and #data.user_achievements[alt] > 0 then
            return true
        end
    end

    -- We removed the forced "true" for test pages to avoid dev-role injection
    return false
end

--------------------------------------------------------------------------------
-- Get a user-friendly name for a given achievement type
--------------------------------------------------------------------------------
function Achievements.getAchievementName(achievementType)
    if not achievementType or achievementType == '' then
        debugLog("Empty achievement type provided to getAchievementName")
        mw.log("ACHIEVEMENT-NAME-ERROR: Received empty achievementType")
        return 'Unknown'
    end

    debugLog("Looking up achievement name for type: '" .. tostring(achievementType) .. "'")

    local data = Achievements.loadData()
    if not data or not data.achievement_types then
        mw.log("ACHIEVEMENT-NAME-ERROR: No achievement data or achievement_types missing")
        return achievementType
    end
    
    -- Log achievement type count to help diagnose issues
    debugLog("Found " .. #data.achievement_types .. " achievement types in data")
    
    -- Try to match achievement ID
    for i, typeData in ipairs(data.achievement_types) do
        if typeData.id == achievementType then
            debugLog("Found match for " .. achievementType .. " at index " .. i)
            
            if typeData.name and typeData.name ~= "" then
                debugLog("Using name '" .. typeData.name .. "' for type '" .. achievementType .. "'")
                return typeData.name
            else
                debugLog("Type " .. achievementType .. " has no name; using ID as fallback")
                return achievementType
            end
        end
    end

    -- If we reach here, no match was found - log all achievement types to help diagnose
    debugLog("No match found for '" .. achievementType .. "' - logging all available types")
    for i, typeData in ipairs(data.achievement_types) do
        debugLog("Available type[" .. i .. "]: id=" .. (typeData.id or "nil") .. 
               ", name=" .. (typeData.name or "nil") ..
               ", type=" .. (typeData.type or "nil"))
    end

    debugLog("No achievement found with type '" .. achievementType .. "'; using ID fallback")
    return achievementType
end

--------------------------------------------------------------------------------
-- Find the top-tier achievement for the user (lowest tier number)
-- Return the CSS class and the readable achievement name
--------------------------------------------------------------------------------
function Achievements.getTitleClass(pageId)
    if not pageId or pageId == '' then
        debugLog("Empty page ID provided to getTitleClass")
        return '', ''
    end

    local data = Achievements.loadData()
    if not data or not data.user_achievements then
        debugLog("No achievement data available in getTitleClass")
        return '', ''
    end

    local key = tostring(pageId)
    debugLog("Looking up achievements for page ID: " .. key)
    
    -- Try to fetch achievements for this pageId
    local userAchievements = {}
    local userAchievementKey = key
    
    -- Try the direct key first
    if data.user_achievements[key] and #data.user_achievements[key] > 0 then
        debugLog("Found achievements directly under key: " .. key)
        userAchievements = data.user_achievements[key]
        userAchievementKey = key
    -- If no achievements found under normal ID, try alternative formats
    elseif key:match("^%d+$") then
        local alternateKeys = {
            "n" .. key,       -- n12345 format
            "page" .. key,    -- page12345 format
            "user" .. key     -- user12345 format
        }
        
        for _, altKey in ipairs(alternateKeys) do
            if data.user_achievements[altKey] and #data.user_achievements[altKey] > 0 then
                debugLog("Found achievements under alternate key: " .. altKey)
                userAchievements = data.user_achievements[altKey]
                userAchievementKey = altKey
                break
            end
        end
    end

    -- Log achievement count and details
    if #userAchievements > 0 then
        debugLog("Found " .. #userAchievements .. " achievements for page ID " .. key .. " under key " .. userAchievementKey)
        for i, ach in ipairs(userAchievements) do
            debugLog("  Achievement " .. i .. ": type=" .. (ach.type or "nil"))
        end
    else
        debugLog("No achievements found for page ID " .. key .. " under any key")
        return '', ''
    end

    -- Find the highest-tier achievement (lowest tier number)
    local highestTier = 999
    local highestAchievement = nil

    for _, achievement in ipairs(userAchievements) do
        local achType = achievement.type
        if not achType then
            debugLog("Achievement missing type property - skipping")
        else
            debugLog("Processing achievement type: " .. achType)
            
            -- Find the achievement definition for this type
            local achDef = nil
            for _, typeData in ipairs(data.achievement_types) do
                if typeData.id == achType then
                    achDef = typeData
                    break
                end
            end
            
            if not achDef then
                debugLog("No definition found for achievement type: " .. achType)
            else
                local tier = achDef.tier or 999
                debugLog("  Found type '" .. achDef.id .. "' with tier " .. tier .. 
                       ", name '" .. (achDef.name or "nil") .. 
                       "', type '" .. (achDef.type or "nil") .. "'")
                
                if tier < highestTier then
                    highestTier = tier
                    highestAchievement = achDef
                    debugLog("  New highest tier achievement: " .. achDef.id)
                end
            end
        end
    end

    if not highestAchievement or not highestAchievement.id then
        debugLog("No valid top-tier achievement found for page ID " .. key)
        return '', ''
    end

    local cssClass = "achievement-" .. highestAchievement.id
    local displayName = highestAchievement.name or highestAchievement.id or "Award"
    
    debugLog("Using top-tier achievement: " .. cssClass .. " with name: " .. displayName)
    
    return cssClass, displayName
end

--------------------------------------------------------------------------------
-- Renders a simple "box" with the top-tier achievement for the user
--------------------------------------------------------------------------------
function Achievements.renderAchievementBox(pageId)
    if not pageId or pageId == '' then
        return ''
    end

    local data = Achievements.loadData()
    if not data or not data.user_achievements then
        return ''
    end

    local key = tostring(pageId)
    local userAchievements = data.user_achievements[key]
    if (not userAchievements or #userAchievements == 0) and key:match("^%d+$") then
        userAchievements = data.user_achievements["n" .. key]
    end

    if not userAchievements or #userAchievements == 0 then
        return ''
    end
    
    -- Build a lookup table for achievement type definitions
    local typeDefinitions = {}
    if data and data.achievement_types then
        for _, typeData in ipairs(data.achievement_types) do
            if typeData.id and typeData.name then
                typeDefinitions[typeData.id] = {
                    name = typeData.name,
                    tier = typeData.tier or 999
                }
            end
        end
    end

    -- Look for the highest-tier achievement (lowest tier number)
    local highestTier = 999
    local topAchType = nil

    for _, achievement in ipairs(userAchievements) do
        local achType = achievement.type
        if typeDefinitions[achType] and typeDefinitions[achType].tier < highestTier then
            highestTier = typeDefinitions[achType].tier
            topAchType = achType
        end
    end

    -- If we found an achievement, render it
    if topAchType and typeDefinitions[topAchType] then
        local achName = typeDefinitions[topAchType].name or topAchType
        
        return string.format(
            '<div class="achievement-box-simple" data-achievement-type="%s">%s</div>',
            topAchType,
            htmlEncode(achName)
        )
    end

    return ''
end

--------------------------------------------------------------------------------
-- Simple pass-through to track pages (for future expansions)
--------------------------------------------------------------------------------
function Achievements.trackPage(pageId, pageName)
    return true
end

--------------------------------------------------------------------------------
-- Retrieve a specific achievement if present, by type
--------------------------------------------------------------------------------
function Achievements.getSpecificAchievement(pageId, achievementType)
    debugLog("ACHIEVEMENT-DEBUG: Looking for '" .. tostring(achievementType) ..
             "' in page ID: " .. tostring(pageId))

    if not pageId or not achievementType or pageId == '' then
        debugLog("ACHIEVEMENT-DEBUG: Invalid arguments for getSpecificAchievement")
        return nil
    end

    local data = Achievements.loadData()
    if not data or not data.user_achievements then
        debugLog("ACHIEVEMENT-DEBUG: No achievement data loaded")
        return nil
    end

    local key = tostring(pageId)
    local userAchievements = data.user_achievements[key] or {}
    
    -- If no achievements found under normal ID, try alternative format
    if #userAchievements == 0 and key:match("^%d+$") then
        local altKey = "n" .. key
        debugLog("No achievements under ID '" .. key .. "', trying alternative ID: '" .. altKey .. "'")
        userAchievements = data.user_achievements[altKey] or {}
    end
    
    -- Direct lookup for the requested achievement type
    for _, achievement in ipairs(userAchievements) do
        if achievement.type == achievementType then
            debugLog("FOUND ACHIEVEMENT: " .. achievementType .. " for user " .. key)
            return achievement
        end
    end

    debugLog("ACHIEVEMENT-DEBUG: No match found for achievement type: " .. achievementType)
    return nil
end

--------------------------------------------------------------------------------
-- Get achievement definition directly from JSON data
--------------------------------------------------------------------------------
function Achievements.getAchievementDefinition(achievementType)
    if not achievementType or achievementType == '' then
        debugLog("ACHIEVEMENT-DEF: Empty achievement type")
        return nil
    end
    
    local data = Achievements.loadData()
    if not data or not data.achievement_types then
        debugLog("ACHIEVEMENT-DEF: No achievement data loaded")
        return nil
    end
    
    -- Direct lookup in achievement_types array
    for _, typeData in ipairs(data.achievement_types) do
        if typeData.id == achievementType then
            debugLog("ACHIEVEMENT-DEF: Found definition for " .. achievementType)
            return typeData
        end
    end
    
    debugLog("ACHIEVEMENT-DEF: No definition found for " .. achievementType)
    return nil
end

--------------------------------------------------------------------------------
-- Diagnostic Function: List all badges for a page with details
--------------------------------------------------------------------------------
function Achievements.debugBadgesForPage(pageId)
    if not pageId or pageId == '' then
        mw.log("BADGE-DEBUG: Empty page ID")
        return "ERROR: No page ID provided"
    end

    local data = Achievements.loadData()
    if not data then
        mw.log("BADGE-DEBUG: Failed to load achievement data")
        return "ERROR: Failed to load achievement data"
    end

    local key = tostring(pageId)
    local userAchievements = data.user_achievements[key] or {}
    
    -- Check alternate keys if needed
    if #userAchievements == 0 and key:match("^%d+$") then
        local altKey = "n" .. key
        userAchievements = data.user_achievements[altKey] or {}
        if #userAchievements > 0 then
            mw.log("BADGE-DEBUG: Found achievements under alternate key: " .. altKey)
        end
    end

    if #userAchievements == 0 then
        mw.log("BADGE-DEBUG: No achievements found for page ID " .. pageId)
        return "No achievements found for page ID " .. pageId
    end

    -- Build debug report
    local output = {}
    table.insert(output, "=== BADGE DEBUG FOR PAGE " .. pageId .. " ===")
    table.insert(output, "Found " .. #userAchievements .. " achievements")
    
    -- Check each achievement in detail
    for i, achievement in ipairs(userAchievements) do
        local achType = achievement.type or "nil"
        local typeDef = Achievements.getAchievementDefinition(achType)
        
        table.insert(output, "\nACHIEVEMENT " .. i .. ":")
        table.insert(output, "  Type: " .. achType)
        
        if typeDef then
            table.insert(output, "  Name: " .. (typeDef.name or "unnamed"))
            table.insert(output, "  Definition Type: " .. (typeDef.type or "unspecified"))
            table.insert(output, "  Description: " .. (typeDef.description or "none"))
            if typeDef.tier then
                table.insert(output, "  Tier: " .. typeDef.tier)
            end
        else
            table.insert(output, "  WARNING: No type definition found!")
        end
    end
    
    return table.concat(output, "\n")
end

--------------------------------------------------------------------------------
-- Find and return title achievement for the user if one exists
-- This specifically looks for achievements with type="title"
-- Return the CSS class, readable achievement name, and achievement ID (or empty strings if none found)
--------------------------------------------------------------------------------
function Achievements.getTitleAchievement(pageId)
    if not pageId or pageId == '' then
        debugLog("Empty page ID provided to getTitleAchievement")
        return '', '', ''
    end

    local data = Achievements.loadData()
    if not data or not data.user_achievements then
        debugLog("No achievement data available in getTitleAchievement")
        return '', '', ''
    end

    local key = tostring(pageId)
    debugLog("Looking up title achievements for page ID: " .. key)
    
    -- Try to fetch achievements for this pageId, checking multiple possible key formats
    local userAchievements = {}
    local userAchievementKey = key
    
    -- Try the direct key first
    if data.user_achievements[key] and #data.user_achievements[key] > 0 then
        debugLog("Found achievements directly under key: " .. key)
        userAchievements = data.user_achievements[key]
        userAchievementKey = key
    -- If no achievements found under normal ID, try alternative formats
    elseif key:match("^%d+$") then
        local alternateKeys = {
            "n" .. key,       -- n12345 format
            "page" .. key,    -- page12345 format
            "user" .. key     -- user12345 format
        }
        
        for _, altKey in ipairs(alternateKeys) do
            if data.user_achievements[altKey] and #data.user_achievements[altKey] > 0 then
                debugLog("Found achievements under alternate key: " .. altKey)
                userAchievements = data.user_achievements[altKey]
                userAchievementKey = altKey
                break
            end
        end
    end

    -- Log achievement count and details for all pages consistently
    if #userAchievements > 0 then
        debugLog("Found " .. #userAchievements .. " achievements for page ID " .. key .. " under key " .. userAchievementKey)
        for i, ach in ipairs(userAchievements) do
            debugLog("  Achievement " .. i .. ": type=" .. (ach.type or "nil"))
        end
    else
        debugLog("No achievements found for page ID " .. key .. " under any key")
        return '', '', ''
    end

    -- Build a table of achievement definitions for quick lookup
    local typeDefinitions = {}
    for _, typeData in ipairs(data.achievement_types) do
        typeDefinitions[typeData.id] = typeData
        debugLog("Loaded definition for type '" .. typeData.id .. 
                "': type=" .. (typeData.type or "nil") .. 
                ", name=" .. (typeData.name or "nil"))
    end

    -- Find title achievements only by strictly checking the "type" field
    local highestTier = 999
    local titleAchievement = nil

    for i, achievement in ipairs(userAchievements) do
        local achType = achievement.type
        if not achType then
            debugLog("Achievement " .. i .. " missing type property - skipping")
        else
            local typeData = typeDefinitions[achType]
            if not typeData then
                debugLog("No definition found for achievement type: " .. achType)
            else
                -- Check if it's a title type ONLY by examining the "type" property
                local achDefType = typeData.type
                debugLog("Checking achievement " .. i .. ": id=" .. achType .. 
                       ", definition type=" .. (achDefType or "nil"))
                
                -- Only consider achievements with type="title"
                if achDefType == "title" then
                    debugLog("Found title achievement: " .. achType)
                    local tier = typeData.tier or 999
                    if tier < highestTier then
                        highestTier = tier
                        titleAchievement = typeData
                        debugLog("Using as highest tier title: " .. typeData.id .. " (tier " .. tier .. ")")
                    end
                else
                    debugLog("Skipping non-title achievement: " .. achType .. " (type=" .. (achDefType or "nil") .. ")")
                end
            end
        end
    end

    if not titleAchievement or not titleAchievement.id then
        debugLog("No valid title achievement found for page ID " .. key)
        return '', '', ''
    end

    local cssClass = "achievement-" .. titleAchievement.id
    local displayName = titleAchievement.name or titleAchievement.id or "Award"
    local achievementId = titleAchievement.id
    
    debugLog("Using title achievement: " .. cssClass .. " with name: " .. displayName)
    
    return cssClass, displayName, achievementId
end

--------------------------------------------------------------------------------
-- Diagnostic function that can be directly called to troubleshoot JSON loading
--------------------------------------------------------------------------------
function Achievements.diagnoseJsonLoading()
    local output = {}
    table.insert(output, "===== ACHIEVEMENT SYSTEM JSON DIAGNOSTICS =====")
    
    -- Check MediaWiki version and capabilities
    table.insert(output, "\n== MEDIAWIKI CAPABILITIES ==")
    if mw.loadJsonData then
        table.insert(output, "✓ mw.loadJsonData: Available")
    else
        table.insert(output, "❌ mw.loadJsonData: Not available! MediaWiki may be too old or misconfigured")
    end
    
    if mw.text and mw.text.jsonDecode then
        table.insert(output, "✓ mw.text.jsonDecode: Available")
    else
        table.insert(output, "❌ mw.text.jsonDecode: Not available! This is a critical function")
    end
    
    -- We don't use Module:JSON anymore, just note that we're using built-in functions
    table.insert(output, "ℹ️ Using MediaWiki's built-in JSON functions")
    
    -- Check the page configuration
    table.insert(output, "\n== JSON PAGE STATUS ==")
    local pageTitle = mw.title.new(ACHIEVEMENT_DATA_PAGE)
    if not pageTitle then
        table.insert(output, "❌ Could not create title object for: " .. ACHIEVEMENT_DATA_PAGE)
        return table.concat(output, "\n")
    end
    
    if not pageTitle.exists then
        table.insert(output, "❌ Page does not exist: " .. ACHIEVEMENT_DATA_PAGE)
        return table.concat(output, "\n")
    end
    
    table.insert(output, "✓ Page exists: " .. ACHIEVEMENT_DATA_PAGE)
    
    -- Check the content model
    if pageTitle.contentModel then
        table.insert(output, "Content model: " .. pageTitle.contentModel)
        if pageTitle.contentModel == "json" then
            table.insert(output, "✓ Page has correct content model: 'json'")
        else
            table.insert(output, "❌ Page has INCORRECT content model: '" .. pageTitle.contentModel .. "' (should be 'json')")
        end
    else
        table.insert(output, "⚠ Could not determine content model")
    end
    
    -- Try to fetch page content
    local content = nil
    local contentSuccess, contentResult = pcall(function()
        return pageTitle:getContent()
    end)
    
    if not contentSuccess or not contentResult or contentResult == "" then
        table.insert(output, "❌ Failed to get page content: " .. tostring(contentResult or "Unknown error"))
        return table.concat(output, "\n")
    end
    
    table.insert(output, "✓ Got page content, length: " .. #contentResult)
    
    -- Check page content
    content = contentResult
    local contentPreview = content:sub(1, 50):gsub("\n", "\\n"):gsub("\t", "\\t")
    table.insert(output, "Content preview: \"" .. contentPreview .. "...\"")
    
    if content:match("^%s*{") then
        table.insert(output, "✓ Content starts with { (correct)")
    else
        table.insert(output, "❌ Content does NOT start with { (INCORRECT)")
    end
    
    -- Check for common issues
    if content:match("^<!DOCTYPE") or content:match("^<[Hh][Tt][Mm][Ll]") then
        table.insert(output, "❌ CRITICAL ERROR: Content appears to be HTML, not JSON!")
    elseif content:match("^%s*<") then
        table.insert(output, "❌ CRITICAL ERROR: Content appears to have XML/HTML markup!")
    end
    
    -- Try mw.loadJsonData
    table.insert(output, "\n== TESTING mw.loadJsonData ==")
    local loadJsonSuccess, loadJsonResult = pcall(function()
        return mw.loadJsonData(ACHIEVEMENT_DATA_PAGE)
    end)
    
    if not loadJsonSuccess then
        table.insert(output, "❌ mw.loadJsonData failed: " .. tostring(loadJsonResult or "Unknown error"))
        
        -- Analyze error
        local errorMsg = tostring(loadJsonResult or "")
        if errorMsg:match("content model") then
            table.insert(output, "ERROR CAUSE: Content model issue - page must be set to 'json' model")
        elseif errorMsg:match("JSON decode") or errorMsg:match("syntax") then
            table.insert(output, "ERROR CAUSE: JSON syntax error - check for invalid JSON formatting")
        elseif errorMsg:match("permission") or errorMsg:match("access") then
            table.insert(output, "ERROR CAUSE: Permission/access error - check page permissions")
        end
    else
        table.insert(output, "✓ mw.loadJsonData SUCCESSFUL!")
        table.insert(output, "Data type: " .. type(loadJsonResult))
        
        if type(loadJsonResult) == "table" then
            local keysFound = {}
            for k, _ in pairs(loadJsonResult) do
                table.insert(keysFound, k)
            end
            table.insert(output, "Top-level keys: " .. table.concat(keysFound, ", "))
            
            if loadJsonResult.achievement_types then
                table.insert(output, "✓ Found " .. #loadJsonResult.achievement_types .. " achievement types")
            else
                table.insert(output, "❌ Missing 'achievement_types' array!")
            end
            
            if loadJsonResult.user_achievements then
                local userCount = 0
                for _, _ in pairs(loadJsonResult.user_achievements) do
                    userCount = userCount + 1
                end
                table.insert(output, "✓ Found user achievements for " .. userCount .. " users")
            else
                table.insert(output, "❌ Missing 'user_achievements' object!")
            end
        end
    end
    
    -- Try direct JSON decoding
    table.insert(output, "\n== TESTING DIRECT JSON DECODING ==")
    
    -- Use mw.text.jsonDecode
    if mw.text and mw.text.jsonDecode then
        local jsonDecodeSuccess, jsonData = pcall(function()
            return mw.text.jsonDecode(content)
        end)
        
        if jsonDecodeSuccess and jsonData then
            table.insert(output, "✓ mw.text.jsonDecode SUCCESSFUL!")
            
            if jsonData.achievement_types then
                table.insert(output, "✓ Found " .. #jsonData.achievement_types .. " achievement types")
            else
                table.insert(output, "❌ Missing 'achievement_types' array in decoded data!")
            end
        else
            table.insert(output, "❌ mw.text.jsonDecode failed: " .. tostring(jsonData or "Unknown error"))
        end
    else
        table.insert(output, "⚠ Cannot test mw.text.jsonDecode (not available)")
    end
    
    -- Use Module:JSON if available
    if json and json.decode then
        local parseSuccess, parsedData = pcall(function()
            return json.decode(content)
        end)
        
        if parseSuccess and parsedData then
            table.insert(output, "✓ Module:JSON's json.decode SUCCESSFUL!")
            
            if parsedData.achievement_types then
                table.insert(output, "✓ Found " .. #parsedData.achievement_types .. " achievement types")
            else
                table.insert(output, "❌ Missing 'achievement_types' array in decoded data!")
            end
        else
            table.insert(output, "❌ Module:JSON's json.decode failed: " .. tostring(parsedData or "Unknown error"))
        end
    else
        table.insert(output, "⚠ Cannot test Module:JSON (not available)")
    end
    
    -- Add recommendations
    table.insert(output, "\n== RECOMMENDATIONS ==")
    if not loadJsonSuccess then
        table.insert(output, "1. Ensure '" .. ACHIEVEMENT_DATA_PAGE .. "' has content model set to 'json'")
        table.insert(output, "2. Verify the JSON is valid with no syntax errors")
        table.insert(output, "3. Check the page starts with { with no leading whitespace or comments")
    else
        table.insert(output, "✓ JSON loading appears to be working correctly!")
    end
    
    return table.concat(output, "\n")
end

return Achievements