_G.LIDCOUNCIL_CreateLootRollStatsHelpers_Impl = function(deps)
  deps = type(deps) == "table" and deps or {}

  local TrimText = deps.TrimText
  local GetLootProfile = deps.GetLootProfile
  local TouchLootProfile = deps.TouchLootProfile
  local TouchLootProfileDomain = deps.TouchLootProfileDomain
  local GetLootLedgerCharacters = deps.GetLootLedgerCharacters
  local DeepCopy = deps.DeepCopy

  local ROLL_STATS_SCHEMA_VERSION = 3

  local function trim(value)
    if type(TrimText) == "function" then
      return TrimText(value)
    end
    local text = tostring(value or "")
    text = text:gsub("^%s+", "")
    text = text:gsub("%s+$", "")
    return text
  end

  local function copy(value)
    if type(DeepCopy) == "function" then
      return DeepCopy(value)
    end
    return value
  end

  local function ResolveOptionalApiFunction(name)
    local api = rawget(_G, "LIDCOUNCIL_API")
    if type(api) == "table" and type(api[name]) == "function" then
      return api[name]
    end
    local fallback = rawget(_G, name)
    if type(fallback) == "function" then
      return fallback
    end
    return nil
  end

  local function TouchLootProfileStats(profile)
    if type(TouchLootProfileDomain) == "function" then
      return TouchLootProfileDomain(profile, "loot")
    end
    local touchDomain = ResolveOptionalApiFunction("LIDCOUNCIL_TouchLootProfileDomain")
    if type(touchDomain) == "function" then
      return touchDomain(profile, "loot")
    end
    if type(TouchLootProfile) == "function" then
      return TouchLootProfile(profile)
    end
  end

  local function BuildDynamicStatsSubdomainKey(prefix, identity)
    local resolvedPrefix = string.lower(trim(prefix))
    local resolvedIdentity = trim(identity)
    if resolvedPrefix == "" or resolvedIdentity == "" then
      return ""
    end
    local token = resolvedIdentity:gsub("([^%w%-%_])", function(character)
      return string.format("%%%02x", string.byte(character))
    end)
    if token == "" then
      return ""
    end
    return resolvedPrefix .. ":" .. token
  end

  local function QueueLootStatsCheckpoint(profileName, subdomain, eventId, eventPayload)
    local queueCheckpoint = ResolveOptionalApiFunction("LIDCOUNCIL_QueueActiveProfileCheckpointFlow")
    if type(queueCheckpoint) ~= "function" then
      return false
    end
    local resolvedSubdomain = trim(subdomain)
    local resolvedEventId = trim(eventId)
    local prebuiltRowsBySubdomain = nil
    local requestedSubdomains = {}
    if resolvedSubdomain ~= "" and resolvedEventId ~= "" then
      local dynamicPrefix = resolvedSubdomain == "lrsv" and "lve" or "lre"
      local eventSubdomain = BuildDynamicStatsSubdomainKey(dynamicPrefix, resolvedEventId)
      if eventSubdomain == "" then
        return false
      end
      local tableField = "entries"
      local remotePayload = {
        deleted = type(eventPayload) ~= "table",
        eventId = resolvedEventId,
      }
      remotePayload.entries = {}
      if type(eventPayload) == "table" then
        remotePayload.entries[resolvedEventId] = copy(eventPayload)
      end
      prebuiltRowsBySubdomain = {
        [eventSubdomain] = {
          {
            id = eventSubdomain,
            subdomain = eventSubdomain,
            applySpec = {
              kind = "keyed_entry",
              tableField = tableField,
              key = resolvedEventId,
            },
            remotePayload = remotePayload,
          },
        },
      }
      requestedSubdomains[1] = eventSubdomain
    else
      requestedSubdomains[1] = "lrsr"
      requestedSubdomains[2] = "lrsv"
    end
    return queueCheckpoint(profileName, {
      reason = "stats_checkpoint",
      domains = { "loot" },
      subdomains = requestedSubdomains,
      prebuiltRowsBySubdomain = prebuiltRowsBySubdomain,
    })
  end

  local function NormalizeStatKey(value, fallback)
    local text = string.lower(trim(value))
    if text == "" then
      text = string.lower(trim(fallback))
    end
    text = text:gsub("%s+", "_")
    text = text:gsub("[^%w_%-]", "")
    if text == "" then
      text = "unknown"
    end
    return text
  end

  local function NormalizeCompactText(value)
    local text = string.lower(trim(value))
    text = text:gsub("[%s%-%_/]+", "")
    return text
  end

  local function IsPassSelection(optionKey, optionLabel)
    local keyText = NormalizeCompactText(optionKey)
    local labelText = NormalizeCompactText(optionLabel)
    return keyText == "pass" or labelText == "pass"
  end

  local function IsNeedSelection(optionKey, optionLabel, pointBehavior, popupSection)
    if IsPassSelection(optionKey, optionLabel) then
      return false
    end

    local section = string.upper(trim(popupSection))
    if section == "NEED" then
      return true
    end
    if section == "GREEDPASS" then
      return false
    end

    local behavior = string.upper(trim(pointBehavior))
    return behavior == "BIS" or behavior == "ITEMLEVEL"
  end

  local function NormalizeEventId(sessionId, itemKey, characterKey)
    local sessionPart = trim(sessionId)
    local itemPart = trim(itemKey)
    local characterPart = trim(characterKey)
    if sessionPart == "" or itemPart == "" or characterPart == "" then
      return ""
    end
    return sessionPart .. ":" .. itemPart .. ":" .. characterPart
  end

  -- Real simulations use TESTSIM item keys and must never affect persistent awards.
  local function IsSimulationStatEvent(eventId, event)
    event = type(event) == "table" and event or {}
    if event.isSimulation == true then
      return true
    end
    local itemKey = string.lower(trim(event.itemKey))
    if string.find(itemKey, "testsim-", 1, true) == 1 then
      return true
    end
    return string.find(string.lower(trim(eventId)), ":testsim-", 1, true) ~= nil
  end

  local function IsSimulationHistoryVote(snapshot, itemKey, entry)
    if type(snapshot) == "table" and snapshot.isSimulation == true then
      return true
    end
    if type(entry) == "table" and entry.isSimulation == true then
      return true
    end
    return IsSimulationStatEvent("", {
      itemKey = trim(itemKey ~= "" and itemKey or (type(entry) == "table" and entry.key) or ""),
    })
  end

  local function EnsureRollStatsBucket(bucket)
    bucket = type(bucket) == "table" and bucket or {}
    bucket.selectionCount = math.max(0, tonumber(bucket.selectionCount) or 0)
    bucket.count = math.max(0, tonumber(bucket.count) or 0)
    bucket.sum = tonumber(bucket.sum) or 0
    bucket.highest = tonumber(bucket.highest)
    bucket.lowest = tonumber(bucket.lowest)
    bucket.firstOptionCount = math.max(0, tonumber(bucket.firstOptionCount) or 0)
    bucket.passCount = math.max(0, tonumber(bucket.passCount) or 0)
    bucket.needCount = math.max(0, tonumber(bucket.needCount) or 0)
    bucket.byOption = type(bucket.byOption) == "table" and bucket.byOption or {}
    return bucket
  end

  local function ApplySelectionCounts(bucket, optionKey, optionLabel, pointBehavior, popupSection, isFirstOption)
    bucket.selectionCount = (tonumber(bucket.selectionCount) or 0) + 1
    if isFirstOption then
      bucket.firstOptionCount = (tonumber(bucket.firstOptionCount) or 0) + 1
    end
    if IsPassSelection(optionKey, optionLabel) then
      bucket.passCount = (tonumber(bucket.passCount) or 0) + 1
    end
    if IsNeedSelection(optionKey, optionLabel, pointBehavior, popupSection) then
      bucket.needCount = (tonumber(bucket.needCount) or 0) + 1
    end
  end

  local function ApplyDiceCounts(bucket, rollValue)
    if not rollValue then
      return
    end
    bucket.count = (tonumber(bucket.count) or 0) + 1
    bucket.sum = (tonumber(bucket.sum) or 0) + rollValue
    bucket.highest = bucket.highest and math.max(bucket.highest, rollValue) or rollValue
    bucket.lowest = bucket.lowest and math.min(bucket.lowest, rollValue) or rollValue
  end

  local function AddSelectionToBucket(bucket, optionKey, optionLabel, pointBehavior, popupSection, rollValue, isFirstOption)
    ApplySelectionCounts(bucket, optionKey, optionLabel, pointBehavior, popupSection, isFirstOption)
    ApplyDiceCounts(bucket, rollValue)

    local optionStatKey = NormalizeStatKey(optionKey, optionLabel)
    bucket.byOption[optionStatKey] = EnsureRollStatsBucket(bucket.byOption[optionStatKey])
    local optionBucket = bucket.byOption[optionStatKey]
    optionBucket.key = optionStatKey
    optionBucket.label = trim(optionLabel) ~= "" and trim(optionLabel) or trim(optionKey)
    ApplySelectionCounts(optionBucket, optionKey, optionLabel, pointBehavior, popupSection, isFirstOption)
    ApplyDiceCounts(optionBucket, rollValue)
  end

  local function RebuildRollStatsTotals(stats)
    stats = type(stats) == "table" and stats or {}
    stats.totals = {}
    for eventId, event in pairs(type(stats.events) == "table" and stats.events or {}) do
      if type(event) == "table" and not IsSimulationStatEvent(eventId, event) then
        local characterKey = trim(event.character or event.c)
        local rollValue = tonumber(event.rollValue or event.r)
        local optionKey = trim(event.optionKey or event.o)
        local optionLabel = trim(event.optionLabel or event.l)
        local optionText = string.lower(trim(optionKey ~= "" and optionKey or optionLabel))
        if characterKey ~= "" and optionText ~= "" and optionText ~= "pending" then
          local pointBehavior = trim(event.pointBehavior or event.p)
          local popupSection = trim(event.popupSection or event.s)
          local isFirstOption = event.isFirstOption == true or event.f == true or event.f == 1
          stats.totals[characterKey] = EnsureRollStatsBucket(stats.totals[characterKey])
          local bucket = stats.totals[characterKey]
          bucket.character = characterKey
          AddSelectionToBucket(bucket, optionKey, optionLabel, pointBehavior, popupSection, rollValue, isFirstOption)
        end
      end
    end
    return stats.totals
  end

  local function RebuildVoteStatsTotals(stats)
    stats = type(stats) == "table" and stats or {}
    stats.voteTotals = {}
    for eventId, event in pairs(type(stats.voteEvents) == "table" and stats.voteEvents or {}) do
      if type(event) == "table" and not IsSimulationStatEvent(eventId, event) then
        local voterKey = trim(event.voter or event.v)
        local targetKey = trim(event.target or event.t)
        if voterKey ~= "" and targetKey ~= "" then
          local bucket = type(stats.voteTotals[voterKey]) == "table" and stats.voteTotals[voterKey] or {}
          bucket.character = voterKey
          bucket.voteCount = (tonumber(bucket.voteCount) or 0) + 1
          stats.voteTotals[voterKey] = bucket
        end
      end
    end
    return stats.voteTotals
  end

  local function EnsureLootRollStats(profile)
    profile = type(profile) == "table" and profile or {}
    profile.lootRollStats = type(profile.lootRollStats) == "table" and profile.lootRollStats or {}
    local stats = profile.lootRollStats
    -- Reading awards must not downgrade a replica imported from a newer schema.
    stats.schemaVersion = math.max(ROLL_STATS_SCHEMA_VERSION, tonumber(stats.schemaVersion) or 0)
    stats.events = type(stats.events) == "table" and stats.events or {}
    stats.totals = type(stats.totals) == "table" and stats.totals or {}
    stats.voteEvents = type(stats.voteEvents) == "table" and stats.voteEvents or {}
    stats.voteTotals = type(stats.voteTotals) == "table" and stats.voteTotals or {}
    RebuildRollStatsTotals(stats)
    RebuildVoteStatsTotals(stats)
    return stats
  end

  local function RecordLootRollStat(profileName, event)
    local profile = type(GetLootProfile) == "function" and select(1, GetLootProfile(profileName)) or nil
    if type(profile) ~= "table" or type(event) ~= "table" then
      return false, "Profile not found."
    end

    local characterKey = trim(event.character or event.memberKey)
    local rollValue = tonumber(event.rollValue)
    local eventId = NormalizeEventId(event.sessionId, event.itemKey, characterKey)
    if eventId == "" then
      return false, "Roll stat event is incomplete."
    end
    if IsSimulationStatEvent(eventId, event) then
      return true, "simulation_ignored"
    end
    local optionKey = trim(event.optionKey)
    local optionLabel = trim(event.optionLabel)
    local optionText = string.lower(trim(optionKey ~= "" and optionKey or optionLabel))
    if optionText == "" or optionText == "pending" then
      return false, "Roll stat event has no roll option."
    end

    local stats = EnsureLootRollStats(profile)
    local nextEvent = {
      itemKey = trim(event.itemKey),
      character = characterKey,
      optionKey = optionKey,
      optionLabel = optionLabel,
      pointBehavior = trim(event.pointBehavior),
      popupSection = trim(event.popupSection),
      rollValue = rollValue,
      selectedAt = tonumber(event.selectedAt) or (time and time() or 0),
      isFirstOption = event.isFirstOption == true,
    }

    local previous = type(stats.events[eventId]) == "table" and stats.events[eventId] or nil
    local unchanged = previous
      and trim(previous.character) == nextEvent.character
      and trim(previous.optionKey) == nextEvent.optionKey
      and trim(previous.optionLabel) == nextEvent.optionLabel
      and trim(previous.pointBehavior) == nextEvent.pointBehavior
      and trim(previous.popupSection) == nextEvent.popupSection
      and tonumber(previous.rollValue) == nextEvent.rollValue
      and previous.isFirstOption == nextEvent.isFirstOption
    if unchanged then
      return true, "unchanged"
    end

    stats.events[eventId] = nextEvent
    stats.updatedAt = tonumber(nextEvent.selectedAt) or (time and time() or 0)
    RebuildRollStatsTotals(stats)
    TouchLootProfileStats(profile)
    QueueLootStatsCheckpoint(profileName, "lrsr", eventId, nextEvent)
    return true, "recorded"
  end

  local function RecordLootVoteStat(profileName, event)
    local profile = type(GetLootProfile) == "function" and select(1, GetLootProfile(profileName)) or nil
    if type(profile) ~= "table" or type(event) ~= "table" then
      return false, "Profile not found."
    end

    local voterKey = trim(event.voter or event.voterKey)
    local eventId = NormalizeEventId(event.sessionId, event.itemKey, voterKey)
    if eventId == "" then
      return false, "Vote stat event is incomplete."
    end
    if IsSimulationStatEvent(eventId, event) then
      return true, "simulation_ignored"
    end

    local stats = EnsureLootRollStats(profile)
    local targetKey = trim(event.target or event.targetKey)
    if targetKey == "" then
      stats.voteEvents[eventId] = nil
    else
      stats.voteEvents[eventId] = {
        itemKey = trim(event.itemKey),
        voter = voterKey,
        target = targetKey,
        votedAt = tonumber(event.votedAt) or (time and time() or 0),
      }
    end
    stats.voteUpdatedAt = tonumber(event.votedAt) or (time and time() or 0)
    RebuildVoteStatsTotals(stats)
    TouchLootProfileStats(profile)
    QueueLootStatsCheckpoint(profileName, "lrsv", eventId, stats.voteEvents[eventId])
    return true, targetKey == "" and "removed" or "recorded"
  end

  local function BuildDiceCandidates(stats, order)
    local rows = {}
    for characterKey, bucket in pairs(type(stats) == "table" and type(stats.totals) == "table" and stats.totals or {}) do
      local count = tonumber(bucket.count) or 0
      local sum = tonumber(bucket.sum) or 0
      if characterKey ~= "" and count > 0 then
        rows[#rows + 1] = {
          character = characterKey,
          count = count,
          average = sum / count,
          highest = tonumber(bucket.highest),
          lowest = tonumber(bucket.lowest),
          selectionCount = tonumber(bucket.selectionCount) or 0,
          firstOptionCount = tonumber(bucket.firstOptionCount) or 0,
          passCount = tonumber(bucket.passCount) or 0,
          needCount = tonumber(bucket.needCount) or 0,
        }
      end
    end
    table.sort(rows, function(a, b)
      if order == "ASC" then
        if a.average == b.average then
          return tostring(a.character) < tostring(b.character)
        end
        return a.average < b.average
      end
      if a.average == b.average then
        return tostring(a.character) < tostring(b.character)
      end
      return a.average > b.average
    end)
    return rows
  end

  local function BuildSelectionCandidates(stats)
    local rows = {}
    for characterKey, bucket in pairs(type(stats) == "table" and type(stats.totals) == "table" and stats.totals or {}) do
      local selectionCount = tonumber(bucket.selectionCount) or 0
      if characterKey ~= "" and selectionCount > 0 then
        rows[#rows + 1] = {
          character = characterKey,
          count = tonumber(bucket.count) or 0,
          selectionCount = selectionCount,
          firstOptionCount = tonumber(bucket.firstOptionCount) or 0,
          passCount = tonumber(bucket.passCount) or 0,
          needCount = tonumber(bucket.needCount) or 0,
        }
      end
    end
    table.sort(rows, function(a, b)
      if (tonumber(a.selectionCount) or 0) == (tonumber(b.selectionCount) or 0) then
        return tostring(a.character or "") < tostring(b.character or "")
      end
      return (tonumber(a.selectionCount) or 0) > (tonumber(b.selectionCount) or 0)
    end)
    return rows
  end

  local function BuildLedgerMap(rows)
    local byCharacter = {}
    for _, row in ipairs(rows or {}) do
      if type(row) == "table" then
        local character = trim(row.character)
        if character ~= "" then
          byCharacter[character] = row
        end
      end
    end
    return byCharacter
  end

  local function BuildVoteCandidates(profile, stats)
    local counts = {}
    local statTotals = type(stats) == "table" and type(stats.voteTotals) == "table" and stats.voteTotals or {}
    for characterKey, bucket in pairs(statTotals) do
      local count = tonumber(type(bucket) == "table" and bucket.voteCount) or 0
      if trim(characterKey) ~= "" and count > 0 then
        counts[trim(characterKey)] = count
      end
    end
    if next(counts) ~= nil then
      local rows = {}
      for character, count in pairs(counts) do
        rows[#rows + 1] = {
          character = character,
          voteCount = tonumber(count) or 0,
        }
      end
      table.sort(rows, function(a, b)
        local av = tonumber(a.voteCount) or 0
        local bv = tonumber(b.voteCount) or 0
        if av == bv then
          return tostring(a.character or "") < tostring(b.character or "")
        end
        return av > bv
      end)
      return rows
    end

    local seenVotes = {}
    local history = type(profile) == "table" and type(profile.liveBoardHistory) == "table" and profile.liveBoardHistory or {}

    local function countVote(snapshot, itemKey, entry, voterKey, targetKey)
      local voter = trim(voterKey)
      local target = trim(targetKey)
      local resolvedItemKey = trim(itemKey ~= "" and itemKey or (type(entry) == "table" and entry.key) or "")
      if voter == "" or target == "" or resolvedItemKey == "" then
        return
      end
      if IsSimulationHistoryVote(snapshot, resolvedItemKey, entry) then
        return
      end
      local boardKey = trim(type(snapshot) == "table" and (snapshot.boardId ~= "" and snapshot.boardId or snapshot.sessionId) or "")
      if boardKey == "" then
        boardKey = tostring(snapshot)
      end
      local voteKey = boardKey .. ":" .. resolvedItemKey .. ":" .. voter
      if seenVotes[voteKey] then
        return
      end
      seenVotes[voteKey] = true
      counts[voter] = (tonumber(counts[voter]) or 0) + 1
    end

    for _, snapshot in ipairs(history) do
      if type(snapshot) == "table" and not snapshot.isSimulation and type(snapshot.items) == "table" then
        local visitedItems = {}
        for _, itemKey in ipairs(type(snapshot.itemOrder) == "table" and snapshot.itemOrder or {}) do
          local resolvedItemKey = trim(itemKey)
          local entry = snapshot.items[resolvedItemKey]
          if type(entry) == "table" then
            visitedItems[resolvedItemKey] = true
            for voterKey, targetKey in pairs(type(entry.votesByVoter) == "table" and entry.votesByVoter or {}) do
              countVote(snapshot, resolvedItemKey, entry, voterKey, targetKey)
            end
          end
        end
        for itemKey, entry in pairs(snapshot.items) do
          local resolvedItemKey = trim(itemKey)
          if type(entry) == "table" and not visitedItems[resolvedItemKey] then
            for voterKey, targetKey in pairs(type(entry.votesByVoter) == "table" and entry.votesByVoter or {}) do
              countVote(snapshot, resolvedItemKey, entry, voterKey, targetKey)
            end
          end
        end
      end
    end

    local rows = {}
    for character, count in pairs(counts) do
      rows[#rows + 1] = {
        character = character,
        voteCount = tonumber(count) or 0,
      }
    end
    table.sort(rows, function(a, b)
      local av = tonumber(a.voteCount) or 0
      local bv = tonumber(b.voteCount) or 0
      if av == bv then
        return tostring(a.character or "") < tostring(b.character or "")
      end
      return av > bv
    end)
    return rows
  end

  local function ValidateSimulationVoteExclusion()
    local stats = {
      voteEvents = {
        ["SESSION-LIVE:LIVE-ITEM:Host-Realm"] = {
          itemKey = "LIVE-ITEM",
          voter = "Host-Realm",
          target = "Target-Realm",
        },
        ["SESSION-SIM:TESTSIM-LOCAL:Host-Realm"] = {
          itemKey = "TESTSIM-LOCAL",
          voter = "Host-Realm",
          target = "Target-Realm",
          isSimulation = true,
        },
        ["SESSION-SIM:TESTSIM-REMOTE:Council-Realm"] = {
          itemKey = "TESTSIM-REMOTE",
          voter = "Council-Realm",
          target = "Target-Realm",
        },
      },
    }
    RebuildVoteStatsTotals(stats)

    local hostVoteCount = tonumber(type(stats.voteTotals["Host-Realm"]) == "table" and stats.voteTotals["Host-Realm"].voteCount) or 0
    local receivedVoteCount = tonumber(type(stats.voteTotals["Council-Realm"]) == "table" and stats.voteTotals["Council-Realm"].voteCount) or 0
    local historyRows = BuildVoteCandidates({
      liveBoardHistory = {
        {
          boardId = "VALIDATION-BOARD",
          items = {
            ["TESTSIM-HISTORY"] = {
              key = "TESTSIM-HISTORY",
              votesByVoter = { ["History-Sim-Realm"] = "Target-Realm" },
            },
            ["LIVE-HISTORY"] = {
              key = "LIVE-HISTORY",
              votesByVoter = { ["History-Real-Realm"] = "Target-Realm" },
            },
          },
          itemOrder = { "TESTSIM-HISTORY", "LIVE-HISTORY" },
        },
      },
    }, { voteTotals = {} })
    local historyCounts = {}
    for _, row in ipairs(historyRows) do
      historyCounts[trim(row.character)] = tonumber(row.voteCount) or 0
    end

    local checks = {
      { name = "real vote remains countable", passed = hostVoteCount == 1 },
      { name = "host-local simulation vote excluded", passed = hostVoteCount == 1 },
      { name = "received simulation vote excluded", passed = receivedVoteCount == 0 },
      { name = "legacy simulation history row excluded", passed = (tonumber(historyCounts["History-Sim-Realm"]) or 0) == 0 },
      { name = "real history vote remains countable", passed = (tonumber(historyCounts["History-Real-Realm"]) or 0) == 1 },
    }
    local passed = true
    for _, check in ipairs(checks) do
      if check.passed ~= true then
        passed = false
        break
      end
    end
    return passed, {
      checks = checks,
      hostVoteCount = hostVoteCount,
      receivedVoteCount = receivedVoteCount,
      historyCounts = historyCounts,
    }
  end

  local function BuildLedgerCandidates(rows, metricKey, filter)
    local candidates = {}
    for _, row in ipairs(rows or {}) do
      if type(row) == "table" and (type(filter) ~= "function" or filter(row)) then
        candidates[#candidates + 1] = row
      end
    end
    table.sort(candidates, function(a, b)
      local av = tonumber(a[metricKey]) or 0
      local bv = tonumber(b[metricKey]) or 0
      if av == bv then
        return tostring(a.character or "") < tostring(b.character or "")
      end
      return av > bv
    end)
    return candidates
  end

  local function FindTopLedgerRow(rows, metricKey, filter)
    local candidates = BuildLedgerCandidates(rows, metricKey, filter)
    return candidates[1], candidates[2], #candidates
  end

  local function FormatAwardCount(value, singular, plural)
    local count = tonumber(value) or 0
    return tostring(count) .. " " .. (count == 1 and singular or plural)
  end

  local function BuildNoWinnerAward(key, title, description)
    return {
      key = key,
      title = title,
      winner = "No winner yet",
      metric = "-",
      detail = description,
      hasWinner = false,
    }
  end

  local function BuildAwardRanking(rows, config)
    config = type(config) == "table" and config or {}
    local rankingRows = {}
    local previousRankKey = nil
    local currentRank = 0
    for index, source in ipairs(type(rows) == "table" and rows or {}) do
      if type(source) == "table" then
        local rankKey = type(config.rankKey) == "function" and tostring(config.rankKey(source) or "") or tostring(index)
        local isTied = index > 1 and rankKey == previousRankKey
        if not isTied then
          currentRank = index
          previousRankKey = rankKey
        end
        local metric = type(config.metric) == "function" and config.metric(source) or "-"
        local detail = type(config.detail) == "function" and config.detail(source) or ""
        local gap = index > 1 and not isTied and type(config.gap) == "function" and config.gap(rows[1], source) or ""
        rankingRows[#rankingRows + 1] = {
          rank = currentRank,
          character = trim(source.character),
          metric = tostring(metric or "-"),
          detail = tostring(detail or ""),
          gap = tostring(gap or ""),
        }
      end
    end
    return {
      metricLabel = trim(config.metricLabel),
      qualification = trim(config.qualification),
      rule = trim(config.rule),
      inverse = config.inverse == true,
      rows = rankingRows,
    }
  end

  local function GetLootRollAwards(profileName)
    local profile = type(GetLootProfile) == "function" and select(1, GetLootProfile(profileName)) or nil
    if type(profile) ~= "table" then
      return { awards = {}, profileName = trim(profileName), generatedAt = time and time() or 0 }
    end
    local stats = EnsureLootRollStats(profile)
    local ledgerRows = type(GetLootLedgerCharacters) == "function" and GetLootLedgerCharacters(profileName) or {}
    local ledgerByCharacter = BuildLedgerMap(ledgerRows)
    local awards = {}
    local rankings = {}

    local luckRows = BuildDiceCandidates(stats, "DESC")
    rankings.luckiest = BuildAwardRanking(luckRows, {
      metricLabel = "Average roll",
      qualification = "At least one recorded roll.",
      rule = "Highest average dice roll wins.",
      rankKey = function(row) return string.format("%.6f", tonumber(row.average) or 0) end,
      metric = function(row) return string.format("%.1f", tonumber(row.average) or 0) end,
      detail = function(row) return FormatAwardCount(row.count, "roll", "rolls") end,
      gap = function(leader, row) return string.format("-%.1f", (tonumber(leader.average) or 0) - (tonumber(row.average) or 0)) end,
    })
    if luckRows[1] then
      local winner = luckRows[1]
      local nextWinner = luckRows[2]
      local distance = nextWinner and (winner.average - nextWinner.average) or nil
      awards[#awards + 1] = {
        key = "luckiest",
        title = "Luckiest Roller",
        winner = winner.character,
        metric = string.format("%.1f average", winner.average),
        detail = "The award goes to the luckiest roller who has the highest dice roll average.",
        hasWinner = true,
      }
    else
      awards[#awards + 1] = BuildNoWinnerAward("luckiest", "Luckiest Roller", "The award goes to the luckiest roller who has the highest dice roll average.")
    end

    local worstRows = BuildDiceCandidates(stats, "ASC")
    rankings.worst = BuildAwardRanking(worstRows, {
      metricLabel = "Average roll (lowest wins)",
      qualification = "At least one recorded roll.",
      rule = "Lowest average dice roll wins.",
      inverse = true,
      rankKey = function(row) return string.format("%.6f", tonumber(row.average) or 0) end,
      metric = function(row) return string.format("%.1f", tonumber(row.average) or 0) end,
      detail = function(row) return FormatAwardCount(row.count, "roll", "rolls") end,
      gap = function(leader, row) return string.format("+%.1f", (tonumber(row.average) or 0) - (tonumber(leader.average) or 0)) end,
    })
    if worstRows[1] then
      local winner = worstRows[1]
      local nextWinner = worstRows[2]
      local distance = nextWinner and (nextWinner.average - winner.average) or nil
      awards[#awards + 1] = {
        key = "worst",
        title = "Worst Roller",
        winner = winner.character,
        metric = string.format("%.1f average", winner.average),
        detail = "The award goes to the raider who has the lowest dice roll average.",
        hasWinner = true,
      }
    else
      awards[#awards + 1] = BuildNoWinnerAward("worst", "Worst Roller", "The award goes to the raider who has the lowest dice roll average.")
    end

    local firstOptionRows = BuildSelectionCandidates(stats)
    table.sort(firstOptionRows, function(a, b)
      local av = tonumber(a.firstOptionCount) or 0
      local bv = tonumber(b.firstOptionCount) or 0
      if av == bv then
        return tostring(a.character or "") < tostring(b.character or "")
      end
      return av > bv
    end)
    local bisRows = {}
    for _, row in ipairs(firstOptionRows) do
      if (tonumber(row.firstOptionCount) or 0) > 0 then
        bisRows[#bisRows + 1] = row
      end
    end
    rankings.bis_everything = BuildAwardRanking(bisRows, {
      metricLabel = "First-option rolls",
      qualification = "At least one first-option roll.",
      rule = "Most selections of the first configured roll option wins.",
      rankKey = function(row) return tostring(tonumber(row.firstOptionCount) or 0) end,
      metric = function(row) return tostring(tonumber(row.firstOptionCount) or 0) end,
      detail = function(row) return FormatAwardCount(row.selectionCount, "total selection", "total selections") end,
      gap = function(leader, row) return "-" .. tostring((tonumber(leader.firstOptionCount) or 0) - (tonumber(row.firstOptionCount) or 0)) end,
    })
    if firstOptionRows[1] and (tonumber(firstOptionRows[1].firstOptionCount) or 0) > 0 then
      local winner = firstOptionRows[1]
      local nextWinner = firstOptionRows[2]
      local distance = (tonumber(winner.firstOptionCount) or 0) - (tonumber(nextWinner and nextWinner.firstOptionCount) or 0)
      awards[#awards + 1] = {
        key = "bis_everything",
        title = "Everything is BiS",
        winner = winner.character,
        metric = tostring(tonumber(winner.firstOptionCount) or 0) .. " first-option roll(s)",
        detail = "The award goes to the player who selected the first roll option on the most different items.",
        hasWinner = true,
      }
    else
      awards[#awards + 1] = BuildNoWinnerAward("bis_everything", "Everything is BiS", "The award goes to the player who selected the first roll option on the most different items.")
    end

    local voteRows = BuildVoteCandidates(profile, stats)
    rankings.boss_voter = BuildAwardRanking(voteRows, {
      metricLabel = "Council votes",
      qualification = "At least one council vote.",
      rule = "Most votes cast wins. One vote per dropped item counts.",
      rankKey = function(row) return tostring(tonumber(row.voteCount) or 0) end,
      metric = function(row) return tostring(tonumber(row.voteCount) or 0) end,
      detail = function(row) return FormatAwardCount(row.voteCount, "vote cast", "votes cast") end,
      gap = function(leader, row) return "-" .. tostring((tonumber(leader.voteCount) or 0) - (tonumber(row.voteCount) or 0)) end,
    })
    if voteRows[1] and (tonumber(voteRows[1].voteCount) or 0) > 0 then
      local winner = voteRows[1]
      local nextWinner = voteRows[2]
      local voteCount = tonumber(winner.voteCount) or 0
      local distance = voteCount - (tonumber(nextWinner and nextWinner.voteCount) or 0)
      awards[#awards + 1] = {
        key = "boss_voter",
        title = "The Boss Voter",
        winner = trim(winner.character),
        metric = tostring(voteCount) .. " vote(s)",
        detail = "The award goes to the player who voted the most. Only one vote per dropped item counts.",
        hasWinner = true,
      }
    else
      awards[#awards + 1] = BuildNoWinnerAward("boss_voter", "The Boss Voter", "The award goes to the player who voted the most. Only one vote per dropped item counts.")
    end

    local transmogRows = BuildLedgerCandidates(ledgerRows, "transmogCount", function(row)
      return (tonumber(row.transmogCount) or 0) > 0
    end)
    rankings.prettiest = BuildAwardRanking(transmogRows, {
      metricLabel = "Transmog items",
      qualification = "At least one awarded Transmog roll.",
      rule = "Most awarded Transmog items wins.",
      rankKey = function(row) return tostring(tonumber(row.transmogCount) or 0) end,
      metric = function(row) return tostring(tonumber(row.transmogCount) or 0) end,
      detail = function(row) return FormatAwardCount(row.itemCount, "item received", "items received") end,
      gap = function(leader, row) return "-" .. tostring((tonumber(leader.transmogCount) or 0) - (tonumber(row.transmogCount) or 0)) end,
    })
    local transmogWinner, transmogNext = transmogRows[1], transmogRows[2]
    if transmogWinner then
      local count = tonumber(transmogWinner.transmogCount) or 0
      local distance = count - (tonumber(transmogNext and transmogNext.transmogCount) or 0)
      awards[#awards + 1] = {
        key = "prettiest",
        title = "Prettiest Raider",
        winner = trim(transmogWinner.character),
        metric = tostring(count) .. " transmog item(s)",
        detail = "The award goes to the player who received the most transmog items.",
        hasWinner = true,
      }
    else
      awards[#awards + 1] = BuildNoWinnerAward("prettiest", "Prettiest Raider", "The award goes to the player who received the most transmog items.")
    end

    local disenchantRows = BuildLedgerCandidates(ledgerRows, "disenchantCount", function(row)
      return (tonumber(row.disenchantCount) or 0) > 0
    end)
    rankings.no_respect = BuildAwardRanking(disenchantRows, {
      metricLabel = "Disenchanted items",
      qualification = "At least one awarded Disenchant roll.",
      rule = "Most awarded Disenchant items wins.",
      rankKey = function(row) return tostring(tonumber(row.disenchantCount) or 0) end,
      metric = function(row) return tostring(tonumber(row.disenchantCount) or 0) end,
      detail = function(row) return FormatAwardCount(row.itemCount, "item received", "items received") end,
      gap = function(leader, row) return "-" .. tostring((tonumber(leader.disenchantCount) or 0) - (tonumber(row.disenchantCount) or 0)) end,
    })
    local disenchantWinner, disenchantNext = disenchantRows[1], disenchantRows[2]
    if disenchantWinner then
      local count = tonumber(disenchantWinner.disenchantCount) or 0
      local distance = count - (tonumber(disenchantNext and disenchantNext.disenchantCount) or 0)
      awards[#awards + 1] = {
        key = "no_respect",
        title = "No Respect for the Item",
        winner = trim(disenchantWinner.character),
        metric = tostring(count) .. " disenchant item(s)",
        detail = "The award goes to the player who disenchanted the most items.",
        hasWinner = true,
      }
    else
      awards[#awards + 1] = BuildNoWinnerAward("no_respect", "No Respect for the Item", "The award goes to the player who disenchanted the most items.")
    end

    local forgottenRows = {}
    for _, row in ipairs(BuildSelectionCandidates(stats)) do
      local ledgerRow = ledgerByCharacter[trim(row.character)] or {}
      if (tonumber(row.needCount) or 0) > 0 and (tonumber(ledgerRow.itemCount) or 0) == 0 then
        row.itemCount = tonumber(ledgerRow.itemCount) or 0
        row.sessionCount = tonumber(ledgerRow.sessionCount) or 0
        forgottenRows[#forgottenRows + 1] = row
      end
    end
    table.sort(forgottenRows, function(a, b)
      local av = tonumber(a.needCount) or 0
      local bv = tonumber(b.needCount) or 0
      if av == bv then
        local as = tonumber(a.sessionCount) or 0
        local bs = tonumber(b.sessionCount) or 0
        if as == bs then
          return tostring(a.character or "") < tostring(b.character or "")
        end
        return as > bs
      end
      return av > bv
    end)
    rankings.forgotten = BuildAwardRanking(forgottenRows, {
      metricLabel = "Need rolls without loot",
      qualification = "At least one Need roll and no awarded items.",
      rule = "Most Need rolls without receiving an item wins.",
      rankKey = function(row)
        return tostring(tonumber(row.needCount) or 0) .. ":" .. tostring(tonumber(row.sessionCount) or 0)
      end,
      metric = function(row) return tostring(tonumber(row.needCount) or 0) end,
      detail = function(row) return "0 items | " .. FormatAwardCount(row.sessionCount, "session", "sessions") end,
      gap = function(leader, row) return "-" .. tostring((tonumber(leader.needCount) or 0) - (tonumber(row.needCount) or 0)) end,
    })
    if forgottenRows[1] then
      local winner = forgottenRows[1]
      local nextWinner = forgottenRows[2]
      local needCount = tonumber(winner.needCount) or 0
      local distance = needCount - (tonumber(nextWinner and nextWinner.needCount) or 0)
      awards[#awards + 1] = {
        key = "forgotten",
        title = "The Forgotten",
        winner = trim(winner.character),
        metric = tostring(needCount) .. " need roll(s), 0 items",
        detail = "The award goes to players who keep rolling need options but still have not received an item.",
        hasWinner = true,
      }
    else
      awards[#awards + 1] = BuildNoWinnerAward("forgotten", "The Forgotten", "The award goes to players who keep rolling need options but still have not received an item.")
    end

    local passRows = {}
    for _, row in ipairs(BuildSelectionCandidates(stats)) do
      local ledgerRow = ledgerByCharacter[trim(row.character)] or {}
      if (tonumber(row.passCount) or 0) > 0 then
        row.itemCount = tonumber(ledgerRow.itemCount) or 0
        passRows[#passRows + 1] = row
      end
    end
    table.sort(passRows, function(a, b)
      local ap = tonumber(a.passCount) or 0
      local bp = tonumber(b.passCount) or 0
      if ap == bp then
        local ai = tonumber(a.itemCount) or 0
        local bi = tonumber(b.itemCount) or 0
        if ai == bi then
          return tostring(a.character or "") < tostring(b.character or "")
        end
        return ai < bi
      end
      return ap > bp
    end)
    rankings.needs_loot = BuildAwardRanking(passRows, {
      metricLabel = "Pass rolls",
      qualification = "At least one Pass roll.",
      rule = "Most Pass rolls wins; fewer received items breaks a tie.",
      rankKey = function(row)
        return tostring(tonumber(row.passCount) or 0) .. ":" .. tostring(tonumber(row.itemCount) or 0)
      end,
      metric = function(row) return tostring(tonumber(row.passCount) or 0) end,
      detail = function(row) return FormatAwardCount(row.itemCount, "item received", "items received") end,
      gap = function(leader, row) return "-" .. tostring((tonumber(leader.passCount) or 0) - (tonumber(row.passCount) or 0)) end,
    })
    if passRows[1] then
      local winner = passRows[1]
      local nextWinner = passRows[2]
      local passCount = tonumber(winner.passCount) or 0
      local distance = passCount - (tonumber(nextWinner and nextWinner.passCount) or 0)
      awards[#awards + 1] = {
        key = "needs_loot",
        title = "Who Needs Loot",
        winner = trim(winner.character),
        metric = tostring(passCount) .. " pass roll(s), " .. tostring(tonumber(winner.itemCount) or 0) .. " item(s)",
        detail = "The award goes to the player with the most pass rolls and the lowest number of received items.",
        hasWinner = true,
      }
    else
      awards[#awards + 1] = BuildNoWinnerAward("needs_loot", "Who Needs Loot", "The award goes to the player with the most pass rolls and the lowest number of received items.")
    end

    local hoarderRows = BuildLedgerCandidates(ledgerRows, "itemCount", function(row)
      return (tonumber(row.itemCount) or 0) > 0
    end)
    rankings.hoarder = BuildAwardRanking(hoarderRows, {
      metricLabel = "Items received",
      qualification = "At least one awarded item.",
      rule = "Most items received wins.",
      rankKey = function(row) return tostring(tonumber(row.itemCount) or 0) end,
      metric = function(row) return tostring(tonumber(row.itemCount) or 0) end,
      detail = function(row) return FormatAwardCount(row.sessionCount, "session", "sessions") end,
      gap = function(leader, row) return "-" .. tostring((tonumber(leader.itemCount) or 0) - (tonumber(row.itemCount) or 0)) end,
    })
    local hoarderWinner, hoarderNext = hoarderRows[1], hoarderRows[2]
    if hoarderWinner then
      local itemCount = tonumber(hoarderWinner.itemCount) or 0
      local distance = itemCount - (tonumber(hoarderNext and hoarderNext.itemCount) or 0)
      awards[#awards + 1] = {
        key = "hoarder",
        title = "Hoarder",
        winner = trim(hoarderWinner.character),
        metric = tostring(itemCount) .. " item(s)",
        detail = "The award goes to the player who has received the most items.",
        hasWinner = true,
      }
    else
      awards[#awards + 1] = BuildNoWinnerAward("hoarder", "Hoarder", "The award goes to the player who has received the most items.")
    end

    local rollEventCount = 0
    for _ in pairs(type(stats.events) == "table" and stats.events or {}) do
      rollEventCount = rollEventCount + 1
    end

    return {
      profileName = trim(profileName),
      generatedAt = time and time() or 0,
      updatedAt = tonumber(stats.updatedAt) or 0,
      rollEventCount = rollEventCount,
      awards = copy(awards),
      rankings = copy(rankings),
    }
  end

  return {
    EnsureLootRollStats = EnsureLootRollStats,
    RecordLootRollStat = RecordLootRollStat,
    RecordLootVoteStat = RecordLootVoteStat,
    GetLootRollAwards = GetLootRollAwards,
    ValidateSimulationVoteExclusion = ValidateSimulationVoteExclusion,
  }
end
