Module:Standingsv3

From elfpedia.eu

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

local M = {}

-- Constant for the maximum number of games per team in the regular season
local MAX_GAMES_PER_TEAM = 12

-- Division assignments (This data is within the module but can be centralized if needed)
local teamDivisions = {
    -- North Division
    ["Rhein Fire"] = "North",
    ["Nordic Storm"] = "North",
    ["Hamburg Sea Devils"] = "North",
    ["Berlin Thunder"] = "North",
    -- South Division
    ["Madrid Bravos"] = "South",
    ["Munich Ravens"] = "South",
    ["Raiders Tirol"] = "South",
    ["Helvetic Mercenaries"] = "South",
    -- West Division
    ["Stuttgart Surge"] = "West",
    ["Frankfurt Galaxy"] = "West",
    ["Paris Musketeers"] = "West",
    ["Cologne Centurions"] = "West",
    -- East Division
    ["Vienna Vikings"] = "East",
    ["Prague Lions"] = "East",
    ["Panthers Wrocław"] = "East",
    ["Fehérvár Enthroners"] = "East",
}

-- Defined colors for division headers
local divisionHeaderColors = {
    North = "#4B04D9",
    West = "#193368",
    South = "#BC1F25",
    East = "#17462D",
}

-- Defined colors for playoff status in the table rows
local playoffRowColors = {
    eliminated = "#FFDDDD",       -- Light red for eliminated teams
    qualified_wildcard = "#E0F0FF", -- Light blue for qualified wildcards
    qualified_division = "#FFF8E0", -- Light yellow for qualified division winners (without bye)
    qualified_bye = "#E0FFE0",      -- Light green for qualified division winners (with bye)
}

-- Playoff symbols are no longer directly added to team names, relying on row colors.
-- Keeping this table for reference or potential future use, but not for direct display in table.
local playoffSymbols = {
    -- Symbols are deprecated for direct display, relying on row colors.
    -- qualified = "✓",
    -- won_division = "✓✓",
    -- homefield_advantage = "ʜ",
    -- eliminated = "†",
}

-- Function to retrieve game data from Cargo
function M.getGameData(season)
    local cargo_query_params = {
        tables = "GameSchedule",
        fields = "AwayTeam, HomeTeam, AwayScore, HomeScore, IF(AwayScore > HomeScore, AwayTeam, IF(HomeScore > AwayScore, HomeTeam, ''))=Winner, IsPlayoff, Week, Phase, PlayoffRound",
        where = "Season=" .. tostring(season) .. " AND Phase='Regular season' AND IsPlayoff=0 AND PlayoffRound=0",
        order_by = "Week ASC, GameID ASC",
        limit = "500" -- Increase limit if more than 500 games per season
    }
    local query = mw.ext.cargo.query(cargo_query_params.tables, cargo_query_params.fields, cargo_query_params)
    return query
end

-- Calculates the last N games of a team as an LWLWW string with colors
function M.calculateTeamLastGames(teamName, allGames, numGames)
    numGames = numGames or 3 -- Default to 3
    local teamGames = {}
    for _, game in ipairs(allGames) do
        -- Ensure the game is completed to have a W/L
        local homeScore = tonumber(game.HomeScore)
        local awayScore = tonumber(game.AwayScore)
        if type(homeScore) == 'number' and type(awayScore) == 'number' then
            if game.AwayTeam == teamName or game.HomeTeam == teamName then
                table.insert(teamGames, game)
            end
        end
    end
    table.sort(teamGames, function(a, b)
        local weekA = tonumber(a.Week) or 0
        local weekB = tonumber(b.Week) or 0
        if weekA ~= weekB then
            return weekA < weekB
        end
        return false
    end)
    local lastGamesOutput = {}
    local gamesCount = 0
    for i = #teamGames, 1, -1 do
        if gamesCount >= numGames then
            break
        end
        local game = teamGames[i]
        local outcome = ""
        local color = ""
        if game.Winner == teamName then
            outcome = "W"
            color = "#008000"
        elseif game.Winner ~= "" then -- if there's a winner and it's not our team, it's a loss
            outcome = "L"
            color = "#FF0000"
        else
            outcome = "" -- Should not happen for completed games with a winner
        end
        if outcome ~= "" then -- Only count games with a defined outcome (W/L)
            table.insert(lastGamesOutput, 1, '<span style="color:' .. color .. '; font-weight:bold;">' .. outcome .. '</span>')
            gamesCount = gamesCount + 1
        end
    end
    if #lastGamesOutput > 0 then
        return table.concat(lastGamesOutput, "")
    else
        return '-'
    end
end

-- Function to calculate team statistics and gather opponents for SOV/SOS
function M.calculateTeamStats(games)
    local teamStats = {}
    
    -- First pass: Initialize teams and collect basic statistics (W/L, PF/PA)
    -- and the history of games for SOV/SOS
    for _, game in ipairs(games) do
        local awayTeam = game.AwayTeam
        local homeTeam = game.HomeTeam
        local awayScore = tonumber(game.AwayScore) or 0
        local homeScore = tonumber(game.HomeScore) or 0
        local winner = game.Winner

        -- Ensure teams are initialized
        if not teamStats[awayTeam] then
            teamStats[awayTeam] = { W = 0, L = 0, PF = 0, PA = 0, DivW = 0, DivL = 0, Name = awayTeam, GamesPlayedHistory = {}, H2HMatches = {}, AwayPF = 0 }
        end
        if not teamStats[homeTeam] then
            teamStats[homeTeam] = { W = 0, L = 0, PF = 0, PA = 0, DivW = 0, DivL = 0, Name = homeTeam, GamesPlayedHistory = {}, H2HMatches = {}, AwayPF = 0 }
        end

        -- Only if the game is completed, update W/L/PF/PA and H2H
        local gamePlayed = (type(tonumber(game.AwayScore)) == 'number' and type(tonumber(game.HomeScore)) == 'number')
        
        if gamePlayed then
            teamStats[awayTeam].PF = teamStats[awayTeam].PF + awayScore
            teamStats[awayTeam].PA = teamStats[awayTeam].PA + homeScore
            teamStats[homeTeam].PF = teamStats[homeTeam].PF + homeScore
            teamStats[homeTeam].PA = teamStats[homeTeam].PA + awayScore

            if winner == awayTeam then
                teamStats[awayTeam].W = teamStats[awayTeam].W + 1
                teamStats[homeTeam].L = teamStats[homeTeam].L + 1
            elseif winner == homeTeam then
                teamStats[homeTeam].W = teamStats[homeTeam].W + 1
                teamStats[awayTeam].L = teamStats[awayTeam].L + 1
            end

            local awayDivision = teamDivisions[awayTeam]
            local homeDivision = teamDivisions[homeTeam]
            if awayDivision and homeDivision and awayDivision == homeDivision then
                if winner == awayTeam then
                    teamStats[awayTeam].DivW = teamStats[awayTeam].DivW + 1
                    teamStats[homeTeam].DivL = teamStats[homeTeam].DivL + 1
                elseif winner == homeTeam then
                    teamStats[homeTeam].DivW = teamStats[homeTeam].DivW + 1
                    teamStats[awayTeam].DivL = teamStats[awayTeam].DivL + 1
                end
            end

            -- Store H2H match details
            if teamStats[awayTeam].H2HMatches[homeTeam] == nil then teamStats[awayTeam].H2HMatches[homeTeam] = { wins = 0, losses = 0, pf = 0, pa = 0, away_pf = 0 } end
            if teamStats[homeTeam].H2HMatches[awayTeam] == nil then teamStats[homeTeam].H2HMatches[awayTeam] = { wins = 0, losses = 0, pf = 0, pa = 0, away_pf = 0 } end

            if winner == awayTeam then
                teamStats[awayTeam].H2HMatches[homeTeam].wins = teamStats[awayTeam].H2HMatches[homeTeam].wins + 1
                teamStats[homeTeam].H2HMatches[awayTeam].losses = teamStats[homeTeam].H2HMatches[awayTeam].losses + 1
            elseif winner == homeTeam then
                teamStats[homeTeam].H2HMatches[awayTeam].wins = teamStats[homeTeam].H2HMatches[awayTeam].wins + 1
                teamStats[awayTeam].H2HMatches[homeTeam].losses = teamStats[awayTeam].H2HMatches[homeTeam].losses + 1
            end

            teamStats[awayTeam].H2HMatches[homeTeam].pf = (teamStats[awayTeam].H2HMatches[homeTeam].pf or 0) + awayScore
            teamStats[awayTeam].H2HMatches[homeTeam].pa = (teamStats[awayTeam].H2HMatches[homeTeam].pa or 0) + homeScore
            teamStats[homeTeam].H2HMatches[awayTeam].pf = (teamStats[homeTeam].H2HMatches[awayTeam].pf or 0) + homeScore
            teamStats[homeTeam].H2HMatches[awayTeam].pa = (teamStats[homeTeam].H2HMatches[awayTeam].pa or 0) + awayScore

            -- H2H Away Points: Points scored by the team as the away team in the H2H match
            if game.AwayTeam == awayTeam then
                teamStats[awayTeam].H2HMatches[homeTeam].away_pf = (teamStats[awayTeam].H2HMatches[homeTeam].away_pf or 0) + awayScore
            end
            if game.AwayTeam == homeTeam then -- Correct H2H away_pf for homeTeam when it was AWAY
                teamStats[homeTeam].H2HMatches[awayTeam].away_pf = (teamStats[homeTeam].H2HMatches[awayTeam].away_pf or 0) + awayScore
            end
        end
        
        -- Store ALL games for SOV/SOS calculation later, regardless of whether they are played
        -- This 'GamesPlayedHistory' will contain all opponents on the schedule.
        table.insert(teamStats[awayTeam].GamesPlayedHistory, { opponent = homeTeam, is_win = (gamePlayed and winner == awayTeam) })
        table.insert(teamStats[homeTeam].GamesPlayedHistory, { opponent = awayTeam, is_win = (gamePlayed and winner == homeTeam) })
    end

    -- Recalculate overall AwayPF accurately after all games are processed
    for teamName, stats in pairs(teamStats) do
        stats.AwayPF = 0 -- Reset to recalculate accurately
        for _, game in ipairs(games) do
            local currentTeamScore = tonumber(game.AwayScore) -- Score if the current team was away
            local opponentTeamScore = tonumber(game.HomeScore) -- Opponent's score if current team was away
            if type(currentTeamScore) == 'number' and type(opponentTeamScore) == 'number' then -- Game was played
                if game.AwayTeam == teamName then
                    stats.AwayPF = stats.AwayPF + currentTeamScore
                end
            end
        end
    end

    -- Second pass for derived statistics (Win%, PD, RemainingGames, SOV, SOS)
    for teamName, stats in pairs(teamStats) do
        stats.WinP = (stats.W + stats.L > 0) and (stats.W / (stats.W + stats.L)) or 0
        stats.DivWinP = (stats.DivW + stats.DivL > 0) and (stats.DivW / (stats.DivW + stats.DivL)) or 0
        stats.PD = stats.PF - stats.PA
        
        -- Number of games played and remaining games
        stats.GamesPlayedCount = stats.W + stats.L
        stats.RemainingGames = MAX_GAMES_PER_TEAM - stats.GamesPlayedCount
        if stats.RemainingGames < 0 then stats.RemainingGames = 0 end -- Cannot be negative

        -- Initialize SOV/SOS fields
        stats.SOV = 0
        stats.SOS = 0

        local defeatedOpponentWins = 0
        local defeatedOpponentLosses = 0
        local allScheduleOpponentWins = 0
        local allScheduleOpponentLosses = 0

        -- Collect data for SOV and SOS
        -- Iterate through the team's entire game history
        for _, gameInfo in ipairs(stats.GamesPlayedHistory) do
            local opponentStats = teamStats[gameInfo.opponent] -- Get the opponent's current W/L
            if opponentStats then
                -- For SOS: Sum W/L of all opponents on the schedule
                allScheduleOpponentWins = allScheduleOpponentWins + opponentStats.W
                allScheduleOpponentLosses = allScheduleOpponentLosses + opponentStats.L

                -- For SOV: Sum W/L only of defeated opponents
                if gameInfo.is_win then -- is_win is true if the team won the game AND it was played
                    defeatedOpponentWins = defeatedOpponentWins + opponentStats.W
                    defeatedOpponentLosses = defeatedOpponentLosses + opponentStats.L
                end
            end
        end
        
        -- Calculate SOV and SOS
        local sov_denom = defeatedOpponentWins + defeatedOpponentLosses
        stats.SOV = (sov_denom > 0) and (defeatedOpponentWins / sov_denom) or 0

        local sos_denom = allScheduleOpponentWins + allScheduleOpponentLosses
        stats.SOS = (sos_denom > 0) and (allScheduleOpponentWins / sos_denom) or 0
    end
    return teamStats
end

-- Tie-breaking function with all rules
function M.compareTeams(statsA, statsB, season, is_for_division_sorting)
    is_for_division_sorting = is_for_division_sorting or false

    -- 1. Number of wins
    if statsA.W ~= statsB.W then
        return statsA.W > statsB.W
    end

    -- DIVISION-SPECIFIC TIE-BREAKER: Win Percentage within Division
    if is_for_division_sorting then
        if statsA.DivWinP ~= statsB.DivWinP then
            return statsA.DivWinP > statsB.DivWinP
        end
    end

    -- 2. Head-to-head matchup (direct wins/losses between A and B)
    local h2h_A = statsA.H2HMatches[statsB.Name]
    local h2h_B = statsB.H2HMatches[statsA.Name]

    local h2h_A_wins = (h2h_A and h2h_A.wins) or 0
    local h2h_B_wins = (h2h_B and h2h_B.wins) or 0

    if (h2h_A_wins + h2h_B_wins) > 0 then
        if h2h_A_wins ~= h2h_B_wins then
            return h2h_A_wins > h2h_B_wins
        end
    end

    -- 3. Strength of Victory (SOV)
    if statsA.SOV ~= statsB.SOV then
        return statsA.SOV > statsB.SOV
    end

    -- 4. Strength of Schedule (SOS)
    if statsA.SOS ~= statsB.SOS then
        return statsA.SOS > statsB.SOS
    end

    -- More specific H2H tie-breakers
    if (h2h_A_wins + h2h_B_wins) > 0 then
        local h2h_A_pf = (h2h_A and h2h_A.pf) or 0
        local h2h_A_pa = (h2h_A and h2h_A.pa) or 0
        local h2h_B_pf = (h2h_B and h2h_B.pf) or 0
        local h2h_B_pa = (h2h_B and h2h_B.pa) or 0

        -- 5. Points difference in head-to-head matchups
        local h2h_A_pd = h2h_A_pf - h2h_A_pa
        local h2h_B_pd = h2h_B_pf - h2h_B_pa
        if h2h_A_pd ~= h2h_B_pd then
            return h2h_A_pd > h2h_B_pd
        end

        -- 6. Points scored at away games of head-to-head matchups
        local h2h_A_away_pf = (h2h_A and h2h_A.away_pf) or 0
        local h2h_B_away_pf = (h2h_B and h2h_B.away_pf) or 0
        if h2h_A_away_pf ~= h2h_B_away_pf then
            return h2h_A_away_pf > h2h_B_away_pf
        end
    end

    -- 7. Total point difference (PD)
    if statsA.PD ~= statsB.PD then
        return statsA.PD > statsB.PD
    end

    -- 8. Total points scored (PF)
    if statsA.PF ~= statsB.PF then
        return statsA.PF > statsB.PF
    end

    -- 9. Points scored at away games (AwayPF) - total points scored by the team in all its away games
    if statsA.AwayPF ~= statsB.AwayPF then
        return statsA.AwayPF > statsB.AwayPF
    end

    -- 10. Coin toss (alphabetical as last resort)
    return statsA.Name < statsB.Name
end


-- Function to determine playoff teams and seeding, as well as elimination
function M.getPlayoffTeams(allTeamStats, season)
    local divisionWinners = {}
    local nonDivisionWinners = {}
    -- playoffStatus stores { status = "...", rank = N, isQualified = bool, isEliminated = bool, isSafeDivWinner = bool, isQualifiedAsDivWinner = bool, hasBye = bool }
    local playoffStatus = {} 

    local divisions = {}
    -- Group teams by division
    for teamName, stats in pairs(allTeamStats) do
        local division = teamDivisions[teamName]
        if division then
            if not divisions[division] then
                divisions[division] = {}
            end
            table.insert(divisions[division], { Team = teamName, Stats = stats, Division = division })
            playoffStatus[teamName] = { isQualified = false, isEliminated = false, isSafeDivWinner = false, isQualifiedAsDivWinner = false, hasBye = false, rank = nil, status = nil }
        else
             -- Initialize teams without division if they are in allTeamStats
             playoffStatus[teamName] = { isQualified = false, isEliminated = false, isSafeDivWinner = false, isQualifiedAsDivWinner = false, hasBye = false, rank = nil, status = nil }
        end
    end

    -- 1. Determine Division Winners and check their secure qualification
    for divName, teamsInDiv in pairs(divisions) do
        if #teamsInDiv > 0 then
            -- Sort Division Teams using division-specific tie-breakers
            table.sort(teamsInDiv, function(a, b)
                return M.compareTeams(a.Stats, b.Stats, season, true)
            end)
            
            local divWinnerCandidate = teamsInDiv[1]
            table.insert(divisionWinners, divWinnerCandidate)

            -- Check for "Secure Division Winner" (mathematically eliminates competitors in division)
            if #teamsInDiv >= 2 then
                local secondPlaceTeam = teamsInDiv[2]
                if divWinnerCandidate.Stats.W > (secondPlaceTeam.Stats.W + secondPlaceTeam.Stats.RemainingGames) then
                    playoffStatus[divWinnerCandidate.Team].isSafeDivWinner = true
                end
            end
        end
    end

    -- Sort Division Winners for overall seeding
    table.sort(divisionWinners, function(a, b)
        return M.compareTeams(a.Stats, b.Stats, season, false)
    end)

    -- Assign playoff status and ranks based on sorted Division Winners
    local currentRank = 1
    for i, divWinner in ipairs(divisionWinners) do
        local teamName = divWinner.Team
        if i <= 2 then -- Top 2 Seeds get a Bye
            playoffStatus[teamName].status = "Div" .. i .. " (Bye)"
            playoffStatus[teamName].rank = currentRank
            playoffStatus[teamName].isQualified = true
            playoffStatus[teamName].isQualifiedAsDivWinner = true
            playoffStatus[teamName].hasBye = true
            -- isSafeDivWinner is set above, remains true here
        else -- Division Winners without a Bye
            playoffStatus[teamName].status = "Div" .. i
            playoffStatus[teamName].rank = currentRank
            playoffStatus[teamName].isQualified = true
            playoffStatus[teamName].isQualifiedAsDivWinner = true
        end
        currentRank = currentRank + 1
    end

    -- Collect non-division winners for Wild Card spots
    for teamName, stats in pairs(allTeamStats) do
        if not playoffStatus[teamName] or not playoffStatus[teamName].isQualified then -- if not already qualified as Div Winner
            table.insert(nonDivisionWinners, { Team = teamName, Stats = stats })
        end
    end

    -- Sort non-division winners for Wild Card seeding
    table.sort(nonDivisionWinners, function(a, b)
        return M.compareTeams(a.Stats, b.Stats, season, false)
    end)

    -- Assign Wild Card spots and check their secure qualification
    local wildCardCount = 0
    -- Use a temporary list to track all qualified teams in correct order
    local allQualifiedTeamsSorted = {}
    for teamName, pInfo in pairs(playoffStatus) do
        if pInfo.isQualified and pInfo.rank then
            table.insert(allQualifiedTeamsSorted, { Team = teamName, Stats = allTeamStats[teamName], PlayoffInfo = pInfo })
        end
    end
    table.sort(allQualifiedTeamsSorted, function(a, b) return a.PlayoffInfo.rank < b.PlayoffInfo.rank end)

    -- Now add the Wildcard candidates
    for i, teamInfo in ipairs(nonDivisionWinners) do
        local teamName = teamInfo.Team
        if not playoffStatus[teamName].isQualified then -- Ensure the team is not already qualified as a Division Winner (redundancy check)
            wildCardCount = wildCardCount + 1
            if wildCardCount <= 2 then -- Top 2 Wildcards
                playoffStatus[teamName].status = "Wild Card"
                playoffStatus[teamName].rank = currentRank
                playoffStatus[teamName].isQualified = true
                table.insert(allQualifiedTeamsSorted, { Team = teamName, Stats = allTeamStats[teamName], PlayoffInfo = playoffStatus[teamName] })
                currentRank = currentRank + 1
            else
                break -- Only consider the top 2 wildcards
            end
        end
    end
    
    -- --- Elimination and calculable qualification logic ---
    local lastPlayoffSpotTeam = nil
    if #allQualifiedTeamsSorted >= 6 then
        lastPlayoffSpotTeam = allQualifiedTeamsSorted[6].Stats -- The 6th seed
    end
    
    local firstNonPlayoffTeam = nil
    -- Find the first non-qualified team in the globally sorted list
    local allTeamsSortedOverall = {}
    for teamName, stats in pairs(allTeamStats) do
        table.insert(allTeamsSortedOverall, { Team = teamName, Stats = stats })
    end
    table.sort(allTeamsSortedOverall, function(a, b)
        return M.compareTeams(a.Stats, b.Stats, season, false) -- Use global sorting
    end)

    for _, teamInfo in ipairs(allTeamsSortedOverall) do
        if not playoffStatus[teamInfo.Team].isQualified then
            firstNonPlayoffTeam = teamInfo.Stats
            break
        end
    end

    -- Check calculable qualification (if not already done by seeding above)
    if firstNonPlayoffTeam then
        local minWinsToQualifySafely = firstNonPlayoffTeam.W + firstNonPlayoffTeam.RemainingGames
        for teamName, pInfo in pairs(playoffStatus) do
            if not pInfo.isQualified and allTeamStats[teamName] then
                if allTeamStats[teamName].W > minWinsToQualifySafely then
                    pInfo.isQualified = true -- Mark as qualified
                end
            end
        end
    end
    
    -- Check calculable elimination
    if lastPlayoffSpotTeam then
        for teamName, pInfo in pairs(playoffStatus) do
            if not pInfo.isQualified and allTeamStats[teamName] then -- Only check if not already qualified
                local teamMaxPossibleWins = allTeamStats[teamName].W + allTeamStats[teamName].RemainingGames
                
                if teamMaxPossibleWins < lastPlayoffSpotTeam.W then
                    pInfo.isEliminated = true
                end
            end
        end
    end

    -- Final check for elimination: If a team has no more games left and is not qualified
    for teamName, pInfo in pairs(playoffStatus) do
        if not pInfo.isQualified and not pInfo.isEliminated and allTeamStats[teamName] then
            if allTeamStats[teamName].GamesPlayedCount >= MAX_GAMES_PER_TEAM then -- All games played
                pInfo.isEliminated = true
            end
        end
    end

    return playoffStatus
end

-- Function to generate the HTML table
function M.generateTable(frame)
    local args = frame:getParent().args
    local season = tonumber(args.season) or 2025
    if not season then
        return "<p>Error: Please provide a season (e.g., {{#invoke:Standingsv3|generateTable|season=2025}})</p>"
    end

    local games = M.getGameData(season)
    local allTeamStats = M.calculateTeamStats(games)
    local playoffStatus = M.getPlayoffTeams(allTeamStats, season) -- Retrieves extended status information

    local divisions_with_teams = {
        North = {},
        South = {},
        West = {},
        East = {}
    }

    -- Assign teams to divisions and update their playoff status
    for teamName, stats in pairs(allTeamStats) do
        local division = teamDivisions[teamName]
        if division then
            table.insert(divisions_with_teams[division], stats)
            -- Add playoff status to the team stats object
            stats.PlayoffInfo = playoffStatus[teamName] or { isQualified = false, isEliminated = false, isSafeDivWinner = false, isQualifiedAsDivWinner = false, hasBye = false, rank = nil, status = nil }
        end
    end

    local rankedDivisions = {}
    for divisionName, teams in pairs(divisions_with_teams) do
        if #teams > 0 then
            table.sort(teams, function(a, b)
                return M.compareTeams(a, b, season, true) -- Sorting within the division
            end)
            rankedDivisions[divisionName] = teams
        else
            rankedDivisions[divisionName] = {}
        end
    end

    local output_parts = {}
    local hasContent = false
    local divisionOrder = {"North", "South", "West", "East"}

    table.insert(output_parts, '<div class="standings-grid-container" style="margin-left: 0; margin-right: auto; overflow-x: auto;">')
    
    for _, divisionName in ipairs(divisionOrder) do
        local teamsInDivision = rankedDivisions[divisionName]
        local headerBgColor = divisionHeaderColors[divisionName] or "#F8F9FA"

        if teamsInDivision and #teamsInDivision > 0 then
            table.insert(output_parts, '<div class="standings-container">')
            hasContent = true
            local division_table_html = {}
            table.insert(division_table_html, '<table class="wikitable sortable">')
            table.insert(division_table_html, '<tr>')
            table.insert(division_table_html, '<th colspan="8" style="background-color:' .. headerBgColor .. '; color:white; font-weight:bold; text-align:center;">' .. divisionName .. ' Division</th>')
            table.insert(division_table_html, '</tr>')
            
            -- Column headers
            table.insert(division_table_html, '<tr>')
            table.insert(division_table_html, '<th>Team</th>')
            table.insert(division_table_html, '<th style="text-align:center;">W</th>')
            table.insert(division_table_html, '<th style="text-align:center;">L</th>')
            table.insert(division_table_html, '<th style="text-align:center;">Win%</th>')
            table.insert(division_table_html, '<th style="text-align:center;">DIV</th>')
            table.insert(division_table_html, '<th class="mobile-hidden-col" style="text-align:center;">PF</th>')
            table.insert(division_table_html, '<th class="mobile-hidden-col" style="text-align:center;">PA</th>')
            table.insert(division_table_html, '<th class="mobile-hidden-col" style="text-align:center;">L3</th>')
            table.insert(division_table_html, '</tr>')

            -- Data rows
            for i, teamStats in ipairs(teamsInDivision) do
                local rowStyle = ""
                local playoffInfo = teamStats.PlayoffInfo -- Access new status information

                -- Set row background color based on playoff status
                -- Priority of status display: Eliminated (by color) > Bye > Division Winner > Wild Card
                if playoffInfo.isEliminated then
                    rowStyle = ' style="background-color:' .. playoffRowColors.eliminated .. ' !important;"'
                elseif playoffInfo.hasBye then -- Has Bye (Seed #1 or #2)
                    rowStyle = ' style="background-color:' .. playoffRowColors.qualified_bye .. ' !important;"'
                elseif playoffInfo.isQualifiedAsDivWinner then -- Qualified as Division Winner (but without Bye)
                    rowStyle = ' style="background-color:' .. playoffRowColors.qualified_division .. ' !important;"'
                elseif playoffInfo.isQualified then -- Qualified as Wild Card
                    rowStyle = ' style="background-color:' .. playoffRowColors.qualified_wildcard .. ' !important;"'
                end
                
                local teamLastGames = M.calculateTeamLastGames(teamStats.Name, games)
                table.insert(division_table_html, '<tr' .. rowStyle .. '>')
                
                -- Team name without any symbols
                local accessibilityLabel = ""
if playoffInfo.isEliminated then
    accessibilityLabel = '<span class="sr-only">(Eliminated from playoff contention)</span>'
elseif playoffInfo.hasBye then
    accessibilityLabel = '<span class="sr-only">(Bye Week, already qualified for semifinals)</span>'
elseif playoffInfo.isQualifiedAsDivWinner then
    accessibilityLabel = '<span class="sr-only">(Division Winner)</span>'
elseif playoffInfo.isQualified then
    accessibilityLabel = '<span class="sr-only">(Qualified for playoffs)</span>'
end

table.insert(division_table_html, '<td>[[' .. teamStats.Name .. ']]' .. accessibilityLabel .. '</td>')

                
                table.insert(division_table_html, '<td style="text-align:center;">' .. teamStats.W .. '</td>')
                table.insert(division_table_html, '<td style="text-align:center;">' .. teamStats.L .. '</td>')
                table.insert(division_table_html, '<td style="text-align:center;">' .. string.format("%.3f", teamStats.WinP) .. '</td>')
                table.insert(division_table_html, '<td style="text-align:center;">' .. teamStats.DivW .. '-' .. teamStats.DivL .. '</td>')
                table.insert(division_table_html, '<td class="mobile-hidden-col" style="text-align:center;">' .. teamStats.PF .. '</td>')
                table.insert(division_table_html, '<td class="mobile-hidden-col" style="text-align:center;">' .. teamStats.PA .. '</td>')
                table.insert(division_table_html, '<td class="mobile-hidden-col" style="text-align:center;">' .. teamLastGames .. '</td>')
                table.insert(division_table_html, '</tr>')
            end
            table.insert(division_table_html, '</table>')
            table.insert(output_parts, table.concat(division_table_html, '\n'))
            table.insert(output_parts, '</div>')
        end
    end
    table.insert(output_parts, '</div>')

    -- Add legend (adapted to use colors only and a two-column layout)
    table.insert(output_parts, '\n<div class="standings-legend" style="margin-top: 1em; display: flow-root;">') -- Use flow-root for self-clearing float container
    table.insert(output_parts, '<span style="font-weight:bold; font-size:1.2em; display:block; margin-bottom:0.5em;">Legend:</span>') -- Changed from h4 to span for less separation
    table.insert(output_parts, '<ul style="list-style-type: none; padding: 0; margin: 0;">')
    
    -- Helper function to generate a list item for the legend
    local function createLegendItem(color, description)
        return '<li style="float: left; width: 48%; margin-right: 2%; margin-bottom: 0.5em; box-sizing: border-box;">' ..
               '<span style="display: inline-block; width: 1em; height: 1em; background-color: ' .. color .. '; border: 1px solid #ccc; vertical-align: middle; margin-right: 0.5em;"></span> ' ..
               description ..
               '</li>'
    end

    table.insert(output_parts, createLegendItem(playoffRowColors.qualified_bye, 'Automatically qualified for Semifinals (Bye Week)'))
    table.insert(output_parts, createLegendItem(playoffRowColors.qualified_division, 'Division Winner'))
    table.insert(output_parts, createLegendItem(playoffRowColors.qualified_wildcard, 'Playoff Spot Clinched'))
    table.insert(output_parts, createLegendItem(playoffRowColors.eliminated, 'Eliminated from Playoff Contention'))
    
    table.insert(output_parts, '</ul>')
    table.insert(output_parts, '</div>\n')

    return table.concat(output_parts, '\n')
end

return M