Jump to content

Module:AchievementSystem: Difference between revisions

// via Wikitext Extension for VSCode
// via Wikitext Extension for VSCode
Line 28: Line 28:
-- We'll use MediaWiki's built-in JSON functions directly, no external module needed
-- We'll use MediaWiki's built-in JSON functions directly, no external module needed
local function jsonDecode(jsonString)
local function jsonDecode(jsonString)
     if not jsonString then return nil end
     if not jsonString then  
        debugLog('ERROR: Empty JSON string provided')
        return nil  
    end
      
      
     if mw.text and mw.text.jsonDecode then
     if mw.text and mw.text.jsonDecode then
Line 36: Line 39:
          
          
         if success and result then
         if success and result then
             return result
             -- 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
         else
             debugLog('ERROR: JSON decode failed: ' .. tostring(result or 'unknown error'))
             debugLog('ERROR: JSON decode failed: ' .. tostring(result or 'unknown error'))
Line 87: Line 101:
-- Normalizes achievement type to handle variants or legacy types
-- Normalizes achievement type to handle variants or legacy types
local function normalizeAchievementType(achievementType)
local function normalizeAchievementType(achievementType)
     if not achievementType then return nil end
     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 it's already a standard type, return it directly
Line 93: Line 113:
       achievementType == "ach1" or  
       achievementType == "ach1" or  
       achievementType == "ach2" or  
       achievementType == "ach2" or  
       achievementType == "ach3" then
       achievementType == "ach3" or
      achievementType == "sponsor-role" then
        debugLog("Achievement type already standardized: " .. achievementType)
         return achievementType
         return achievementType
     end
     end
      
      
     -- Otherwise check the mapping table
     -- Otherwise check the mapping table
     return ACHIEVEMENT_TYPE_MAPPING[achievementType] or achievementType
     local mappedType = ACHIEVEMENT_TYPE_MAPPING[achievementType] or achievementType
   
    if mappedType ~= achievementType then
        debugLog("Mapped legacy achievement type '" .. achievementType .. "' to '" .. mappedType .. "'")
    end
   
    return mappedType
end
end


Line 113: Line 141:
     end
     end


     local success, data = pcall(function()
     local data = DEFAULT_DATA
        -- Try using the specialized JSON data loader - this is the correct MediaWiki way
    local jsonLoadingMethod = "none"
        -- to load JSON from wiki pages
   
        mw.log("JSON-DEBUG: ===== LOAD JSON DATA ATTEMPT START =====")
    -- SIMPLIFIED LOADING APPROACH - prioritizing mw.text.jsonDecode as it correctly processes achievement types
        mw.log("JSON-DEBUG: Attempting to use mw.loadJsonData for " .. ACHIEVEMENT_DATA_PAGE)
    mw.log("JSON-DEBUG: === SIMPLIFIED JSON LOADING START ===")
       
   
        -- Check if mw.loadJsonData is available
    -- First get the title and check if page exists
        if not mw.loadJsonData then
    local pageTitle = mw.title.new(ACHIEVEMENT_DATA_PAGE)
            mw.log("JSON-DEBUG: CRITICAL ERROR: mw.loadJsonData function does not exist!")
    if not pageTitle or not pageTitle.exists then
            mw.log("JSON-DEBUG: This indicates the MediaWiki version may be too old or misconfigured")
        mw.log("JSON-DEBUG: " .. ACHIEVEMENT_DATA_PAGE .. " does not exist or title creation failed")
        else
        return DEFAULT_DATA
            mw.log("JSON-DEBUG: mw.loadJsonData function exists, attempting to use it")
    end
        end
   
       
    mw.log("JSON-DEBUG: Page exists with content model: " .. tostring(pageTitle.contentModel))
        local loadJsonSuccess, jsonData = pcall(function()
   
            return mw.loadJsonData(ACHIEVEMENT_DATA_PAGE)
    -- First attempt: Use direct raw content with mw.text.jsonDecode
        end)
    -- Based on diagnostic output, this is the most reliable method
 
    if mw.text and mw.text.jsonDecode then
        if loadJsonSuccess and jsonData then
            mw.log("JSON-DEBUG: Successfully loaded data with mw.loadJsonData")
            mw.log("JSON-DEBUG: JSON data type: " .. type(jsonData))
           
            -- Check if we got a table as expected
            if type(jsonData) == 'table' then
                -- Check some expected fields to confirm it's valid
                if jsonData.achievement_types then
                    mw.log("JSON-DEBUG: ✓ Found achievement_types array with " ..
                          #(jsonData.achievement_types or {}) .. " entries")
                else
                    mw.log("JSON-DEBUG: ✗ Missing achievement_types array in loaded data!")
                end
               
                if jsonData.user_achievements then
                    local userCount = 0
                    for _, _ in pairs(jsonData.user_achievements) do
                        userCount = userCount + 1
                    end
                    mw.log("JSON-DEBUG: ✓ Found user_achievements for " .. userCount .. " users")
                else
                    mw.log("JSON-DEBUG: ✗ Missing user_achievements in loaded data!")
                end
            end
           
            return jsonData
        else
            mw.log("JSON-DEBUG: mw.loadJsonData failed: " .. tostring(jsonData or 'unknown error'))
           
            -- Try to categorize the error
            local errorMsg = tostring(jsonData or '')
            if errorMsg:match("content model") then
                mw.log("JSON-DEBUG: ERROR CAUSE: Content model issue - page must be set to 'json' model")
            elseif errorMsg:match("JSON decode") or errorMsg:match("syntax") then
                mw.log("JSON-DEBUG: ERROR CAUSE: JSON syntax error - check for invalid JSON formatting")
            elseif errorMsg:match("permission") or errorMsg:match("access") then
                mw.log("JSON-DEBUG: ERROR CAUSE: Permission/access error - check page permissions")
            end
        end
       
        -- Try to fetch the raw content to see what's being received
        local rawContent = nil
        pcall(function()
            local title = mw.title.new(ACHIEVEMENT_DATA_PAGE)
            if title and title.exists then
                rawContent = title:getContent()
                if rawContent and #rawContent > 0 then
                    mw.log("JSON-DEBUG: Raw content first 100 chars: " ..  
                          rawContent:sub(1, 100):gsub("\n", "\\n"):gsub("\t", "\\t"))
                         
                    -- Check if it starts with a curly brace
                    if rawContent:match("^%s*{") then
                        mw.log("JSON-DEBUG: ✓ Content starts with { (possibly with whitespace)")
                    else
                        mw.log("JSON-DEBUG: ✗ Content does NOT start with {")
                    end
                   
                    -- Check for common issues
                    if rawContent:match("^<!DOCTYPE") or rawContent:match("^<[Hh][Tt][Mm][Ll]") then
                        mw.log("JSON-DEBUG: ERROR: Content appears to be HTML, not JSON!")
                    elseif rawContent:match("^%s*<") then
                        mw.log("JSON-DEBUG: ERROR: Content appears to have XML/HTML markup!")
                    end
                else
                    mw.log("JSON-DEBUG: Could not get raw content or content is empty")
                end
            else
                mw.log("JSON-DEBUG: Could not create title object or page doesn't exist")
            end
        end)
       
        mw.log("JSON-DEBUG: ===== LOAD JSON DATA ATTEMPT END =====")
       
        -- GET RAW CONTENT DIRECTLY APPROACH - likely the most reliable method
        mw.log("JSON-DEBUG: ===== DIRECT RAW CONTENT METHOD START =====")
       
        -- Try to fetch the raw content directly with proper error handling
        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
       
        -- Get raw content from the wiki page
         local content = nil
         local content = nil
         local contentSuccess, contentResult = pcall(function()
         local contentSuccess, contentResult = pcall(function()
Line 223: Line 167:
             content = contentResult
             content = contentResult
             mw.log("JSON-DEBUG: Successfully retrieved raw content, length: " .. #content)
             mw.log("JSON-DEBUG: Successfully retrieved raw content, length: " .. #content)
           
            -- Log the first part of content for debugging
            local contentPreview = content:sub(1, 100):gsub("\n", "\\n"):gsub("\t", "\\t")
            mw.log("JSON-DEBUG: Content preview: " .. contentPreview)
              
              
             -- Remove any BOM or leading whitespace that might cause issues
             -- Remove any BOM or leading whitespace that might cause issues
Line 235: Line 175:
             end
             end
              
              
             -- Check if content starts with { or [
             local jsonDecodeSuccess, jsonData = pcall(function()
            if content:match("^%s*[{%[]") then
                 return mw.text.jsonDecode(content)
                 mw.log("JSON-DEBUG: Content appears to be valid JSON")
            end)
               
           
                 -- Use mw.text.jsonDecode for parsing
            if jsonDecodeSuccess and jsonData and type(jsonData) == 'table' then
                 if mw.text and mw.text.jsonDecode then
                 -- Validate the structure
                     local jsonDecodeSuccess, jsonData = pcall(function()
                 if jsonData.achievement_types and #jsonData.achievement_types > 0 and jsonData.user_achievements then
                        mw.log("JSON-DEBUG: Using mw.text.jsonDecode on raw content")
                     mw.log("JSON-DEBUG: ✓ Successfully decoded with mw.text.jsonDecode, found " ..
                        return mw.text.jsonDecode(content)
                          #jsonData.achievement_types .. " achievement types")
                    end)
                      
                      
                     if jsonDecodeSuccess and jsonData then
                     -- Log achievement types
                        mw.log("JSON-DEBUG: ✓ Successfully decoded content with mw.text.jsonDecode")
                    for i, typeData in ipairs(jsonData.achievement_types) do
                       
                        mw.log("JSON-DEBUG: Type[" .. i .. "]: id=" .. (typeData.id or "nil") ..
                        -- Validate the structure
                              ", name=" .. (typeData.name or "nil") ..  
                        if jsonData.achievement_types and jsonData.user_achievements then
                              ", type=" .. (typeData.type or "nil"))
                            mw.log("JSON-DEBUG: ✓ Parsed JSON has required fields")
                            mw.log("JSON-DEBUG: ===== DIRECT RAW CONTENT METHOD SUCCESS =====")
                            return jsonData
                        else
                            mw.log("JSON-DEBUG: ✗ Parsed JSON missing critical fields")
                        end
                    else
                        mw.log("JSON-DEBUG: ✗ mw.text.jsonDecode failed: " .. tostring(jsonData or 'unknown error'))
                     end
                     end
                   
                    data = jsonData
                    jsonLoadingMethod = "mw.text.jsonDecode"
                 else
                 else
                     mw.log("JSON-DEBUG: ✗ mw.text.jsonDecode not available")
                     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
                 end
               
                -- No Module:JSON fallback needed - we rely fully on mw.text.jsonDecode
                mw.log("JSON-DEBUG: No fallback method available, relying on mw.text.jsonDecode")
             else
             else
                 mw.log("JSON-DEBUG: ✗ Content does not appear to be valid JSON")
                 mw.log("JSON-DEBUG: ✗ mw.text.jsonDecode failed: " ..  
                mw.log("JSON-DEBUG: First 10 characters: " .. content:sub(1, 10):gsub("\n", "\\n"):gsub("\t", "\\t"))
                      tostring(jsonData or 'unknown error'))
             end
             end
         else
         else
             mw.log("JSON-DEBUG: ✗ Failed to get content: " .. tostring(contentResult or 'unknown error'))
             mw.log("JSON-DEBUG: ✗ Failed to get raw content: " ..  
                  tostring(contentResult or 'unknown error'))
         end
         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")
          
          
         mw.log("JSON-DEBUG: ===== DIRECT RAW CONTENT METHOD FAILED =====")
         local loadJsonSuccess, jsonData = pcall(function()
            return mw.loadJsonData(ACHIEVEMENT_DATA_PAGE)
        end)
          
          
         -- FALLBACK SAFETY APPROACHES
         if loadJsonSuccess and jsonData and type(jsonData) == 'table' then
       
            -- Validate the structure
        -- Try parser cache as fallback
            if jsonData.achievement_types and #jsonData.achievement_types > 0 and jsonData.user_achievements then
        local loadDataSuccess, cachedData = pcall(function()
                mw.log("JSON-DEBUG: ✓ Successfully loaded with mw.loadJsonData, found " ..
             mw.log("JSON-DEBUG: Attempting mw.loadData fallback")
                      #jsonData.achievement_types .. " achievement types")
            return mw.loadData('Module:AchievementSystem')
               
        end)
                data = jsonData
 
                jsonLoadingMethod = "mw.loadJsonData"
        if loadDataSuccess and cachedData then
             else
            mw.log("JSON-DEBUG: Using mw.loadData cached data")
                mw.log("JSON-DEBUG: mw.loadJsonData result missing required fields or empty achievement_types")
             return cachedData
                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
         else
             mw.log("JSON-DEBUG: mw.loadData failed, using default data")
             mw.log("JSON-DEBUG: mw.loadJsonData failed: " ..
                  tostring(jsonData or 'unknown error'))
         end
         end
       
        -- As absolute last resort, use local default data
        mw.log("JSON-DEBUG: All JSON loading approaches failed, using default data")
    end)
    if not success or not data then
        mw.log("JSON-DEBUG: Critical error in load process: " .. tostring(data or 'unknown error'))
        data = DEFAULT_DATA
     end
     end
 
   
     -- Show success source in log
     -- Log which method we used and validation
    mw.log("JSON-DEBUG: JSON loading complete using method: " .. jsonLoadingMethod)
     if data ~= DEFAULT_DATA then
     if data ~= DEFAULT_DATA then
         mw.log("JSON-DEBUG: Successfully loaded JSON data with " .. tostring(#(data.achievement_types or {})) .. " achievement types")
        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")
          
          
         -- Log actual achievement types if DEBUG_MODE
         -- Validate achievement types have required fields
         if DEBUG_MODE and data.achievement_types then
         if data.achievement_types then
             for i, type in ipairs(data.achievement_types) do
             for i, typeData in ipairs(data.achievement_types) do
                 mw.log("JSON-DEBUG: Type[" .. i .. "]: id=" .. (type.id or "nil") .. ", name=" .. (type.name or "nil"))
                 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
         end
         end
    else
        mw.log("JSON-DEBUG: WARNING: Using default data due to loading failures")
     end
     end
 
   
    mw.log("JSON-DEBUG: === SIMPLIFIED JSON LOADING END ===")
   
     dataCache = data
     dataCache = data
     return data
     return data
Line 364: Line 331:
     end
     end
      
      
     -- Display available achievement types in the log
     -- Log achievement type count to help diagnose issues
     mw.log("ACHIEVEMENT-NAME-DEBUG: Searching achievement types for: " .. achievementType)
     debugLog("Found " .. #data.achievement_types .. " achievement types in data")
    mw.log("ACHIEVEMENT-NAME-DEBUG: Available achievement types:")
      
    for i, typeData in ipairs(data.achievement_types) do
        mw.log("  " .. i .. ": id=" .. (typeData.id or "nil") ..
              ", name=" .. (typeData.name or "nil") ..
              ", tier=" .. (typeData.tier or "nil"))
     end
 
     -- Try to match achievement ID
     -- Try to match achievement ID
     for i, typeData in ipairs(data.achievement_types) do
     for i, typeData in ipairs(data.achievement_types) do
         if typeData.id == achievementType then
         if typeData.id == achievementType then
            debugLog("Found match for " .. achievementType .. " at index " .. i)
           
             if typeData.name and typeData.name ~= "" then
             if typeData.name and typeData.name ~= "" then
                 mw.log("ACHIEVEMENT-NAME-SUCCESS: Found achievement: " .. typeData.id .. " with name: " .. typeData.name)
                 debugLog("Using name '" .. typeData.name .. "' for type '" .. achievementType .. "'")
                 return typeData.name
                 return typeData.name
             else
             else
                 mw.log("ACHIEVEMENT-NAME-WARNING: '" .. typeData.id .. "' has no name; using ID")
                 debugLog("Type " .. achievementType .. " has no name; using ID as fallback")
                 return achievementType
                 return achievementType
             end
             end
Line 386: Line 349:
     end
     end


     -- Special case for dev-role lookup - always show details
     -- If we reach here, no match was found - log all achievement types to help diagnose
     if achievementType == "dev-role" then
     debugLog("No match found for '" .. achievementType .. "' - logging all available types")
        mw.log("ACHIEVEMENT-NAME-WARNING: Could not find dev-role in achievement_types!")
    for i, typeData in ipairs(data.achievement_types) do
        mw.log("ACHIEVEMENT-NAME-DEBUG: Raw achievement_types:")
        debugLog("Available type[" .. i .. "]: id=" .. (typeData.id or "nil") ..  
       
              ", name=" .. (typeData.name or "nil") ..
        -- Log achievement types directly for debugging
              ", type=" .. (typeData.type or "nil"))
        pcall(function()
            local debugInfo = "["
            for i, typeData in ipairs(data.achievement_types) do
                debugInfo = debugInfo .. "{ id=" .. (typeData.id or "nil")  
                        .. ", name=" .. (typeData.name or "nil") .. " }"
                if i < #data.achievement_types then
                    debugInfo = debugInfo .. ", "
                end
            end
            debugInfo = debugInfo .. "]"
            mw.log(debugInfo)
        end)
     end
     end


     mw.log("ACHIEVEMENT-NAME-ERROR: No achievement found with type '" .. achievementType .. "'; using ID fallback")
     debugLog("No achievement found with type '" .. achievementType .. "'; using ID fallback")
     return achievementType
     return achievementType
end
end
Line 427: Line 378:


     local key = tostring(pageId)
     local key = tostring(pageId)
     debugLog("Looking up achievements for ID: " .. key)
     debugLog("Looking up achievements for page ID: " .. key)
      
      
    -- Debug logging to show available achievements in JSON
    if data.user_achievements then
        local availableIds = {}
        for k, _ in pairs(data.user_achievements) do
            table.insert(availableIds, k)
        end
        debugLog("Available user IDs in JSON: " .. table.concat(availableIds, ", "))
    end
     -- Try to fetch achievements for this pageId
     -- Try to fetch achievements for this pageId
     local userAchievements = data.user_achievements[key] or {}
     local userAchievements = {}
    local userAchievementKey = key
      
      
     -- If no achievements found under normal ID, try alternative format
    -- Try the direct key first
     if #userAchievements == 0 and key:match("^%d+$") then
    if data.user_achievements[key] and #data.user_achievements[key] > 0 then
         local altKey = "n" .. key
        debugLog("Found achievements directly under key: " .. key)
        debugLog("No achievements under ID '" .. key .. "', trying alternative ID: '" .. altKey .. "'")
        userAchievements = data.user_achievements[key]
        userAchievements = data.user_achievements[altKey] or {}
        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
     end


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


    -- Find the highest-tier achievement (lowest tier number)
     local highestTier = 999
     local highestTier = 999
     local highestAchievement = nil
     local highestAchievement = nil
Line 467: Line 424:
     for _, achievement in ipairs(userAchievements) do
     for _, achievement in ipairs(userAchievements) do
         local achType = achievement.type
         local achType = achievement.type
         debugLog("Processing achievement type: " .. (achType or "nil"))
         if not achType then
       
            debugLog("Achievement missing type property - skipping")
        for _, typeData in ipairs(data.achievement_types) do
        else
            if typeData.id == achType then
            debugLog("Processing achievement type: " .. achType)
                 local tier = typeData.tier or 999
           
                 debugLog("  Found type '" .. typeData.id .. "' with tier " .. tier .. " and name '" .. (typeData.name or "nil") .. "'")
            -- 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
                 if tier < highestTier then
                     highestTier = tier
                     highestTier = tier
                     highestAchievement = typeData
                     highestAchievement = achDef
                     debugLog("  New highest tier achievement: " .. typeData.id)
                     debugLog("  New highest tier achievement: " .. achDef.id)
                 end
                 end
             end
             end
Line 483: Line 456:


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


     local key = tostring(pageId)
     local key = tostring(pageId)
     debugLog("Looking up title achievements for ID: " .. key)
     debugLog("Looking up title achievements for page ID: " .. key)
      
      
     -- Try to fetch achievements for this pageId
     -- Try to fetch achievements for this pageId, checking multiple possible key formats
     local userAchievements = data.user_achievements[key] or {}
     local userAchievements = {}
    local userAchievementKey = key
      
      
     -- If no achievements found under normal ID, try alternative format
    -- Try the direct key first
     if #userAchievements == 0 and key:match("^%d+$") then
    if data.user_achievements[key] and #data.user_achievements[key] > 0 then
         local altKey = "n" .. key
        debugLog("Found achievements directly under key: " .. key)
        debugLog("No achievements under ID '" .. key .. "', trying alternative ID: '" .. altKey .. "'")
        userAchievements = data.user_achievements[key]
        userAchievements = data.user_achievements[altKey] or {}
        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
     end


     -- Log processing for all pages consistently
     -- Log achievement count and details for all pages consistently
    mw.log("TITLE-ACHIEVEMENT-DEBUG: Processing page " .. key)
 
    -- Log found achievements for debugging
     if #userAchievements > 0 then
     if #userAchievements > 0 then
         debugLog("Found " .. #userAchievements .. " achievements for user " .. key)
         debugLog("Found " .. #userAchievements .. " achievements for page ID " .. key .. " under key " .. userAchievementKey)
          
         for i, ach in ipairs(userAchievements) do
        -- Log each achievement for the test page
            debugLog(" Achievement " .. i .. ": type=" .. (ach.type or "nil"))
        if key == "18451" then
            for i, ach in ipairs(userAchievements) do
                mw.log("TITLE-ACHIEVEMENT-DEBUG: Test page achievement " .. i .. " type=" .. (ach.type or "nil"))
            end
         end
         end
     else
     else
         debugLog("No achievements found for user " .. key)
         debugLog("No achievements found for page ID " .. key .. " under any key")
         return '', '', ''
         return '', '', ''
     end
     end
Line 743: Line 726:
     for _, typeData in ipairs(data.achievement_types) do
     for _, typeData in ipairs(data.achievement_types) do
         typeDefinitions[typeData.id] = typeData
         typeDefinitions[typeData.id] = typeData
          
         debugLog("Loaded definition for type '" .. typeData.id ..  
        -- Log definitions for test page
                "': type=" .. (typeData.type or "nil") ..  
        if key == "18451" then
                ", name=" .. (typeData.name or "nil"))
            mw.log("TITLE-ACHIEVEMENT-DEBUG: Definition [" .. typeData.id .. "]: type=" ..  
                  (typeData.type or "nil") .. ", name=" .. (typeData.name or "nil"))
        end
     end
     end


     -- Detailed logging for all achievement checks
     -- Find title achievements only by strictly checking the "type" field
    mw.log("ACHIEVEMENT-DATA: Found " .. #userAchievements .. " achievements for page ID " .. key)
    for i, ach in ipairs(userAchievements) do
        mw.log("ACHIEVEMENT-DATA: Achievement " .. i .. " type=" .. (ach.type or "nil"))
    end
 
    -- Find title achievements only
     local highestTier = 999
     local highestTier = 999
     local titleAchievement = nil
     local titleAchievement = nil
Line 763: Line 737:
     for i, achievement in ipairs(userAchievements) do
     for i, achievement in ipairs(userAchievements) do
         local achType = achievement.type
         local achType = achievement.type
         if achType then
         if not achType then
            debugLog("Achievement " .. i .. " missing type property - skipping")
        else
             local typeData = typeDefinitions[achType]
             local typeData = typeDefinitions[achType]
             if typeData then
             if not typeData then
                 -- Check if it's a title type by examining "type" property, but also handle legacy data
                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
                 local achDefType = typeData.type
                  
                 debugLog("Checking achievement " .. i .. ": id=" .. achType ..  
                -- Log processing details
                      ", definition type=" .. (achDefType or "nil"))
                mw.log("TITLE-ACHIEVEMENT-DEBUG: Checking achievement " .. i .. ": type=" .. achType ..  
                      ", definition type=" .. (achDefType or "nil"))
                  
                  
                 -- Only consider achievements with type="title"
                 -- Only consider achievements with type="title"
                 if achDefType == "title" then
                 if achDefType == "title" then
                     mw.log("TITLE-ACHIEVEMENT-DEBUG: Found potential title achievement: " .. achType)
                     debugLog("Found title achievement: " .. achType)
                     local tier = typeData.tier or 999
                     local tier = typeData.tier or 999
                     if tier < highestTier then
                     if tier < highestTier then
                         highestTier = tier
                         highestTier = tier
                         titleAchievement = typeData
                         titleAchievement = typeData
                         mw.log("TITLE-ACHIEVEMENT-DEBUG: Using as highest tier: " .. typeData.id)
                         debugLog("Using as highest tier title: " .. typeData.id .. " (tier " .. tier .. ")")
                     end
                     end
                else
                    debugLog("Skipping non-title achievement: " .. achType .. " (type=" .. (achDefType or "nil") .. ")")
                 end
                 end
            else
                mw.log("TITLE-ACHIEVEMENT-DEBUG: No definition found for: " .. achType)
             end
             end
        else
            mw.log("TITLE-ACHIEVEMENT-DEBUG: Achievement missing type property")
         end
         end
     end
     end


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

Revision as of 20:13, 1 April 2025

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('&', '&amp;')
            :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