_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 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 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 tableField = resolvedSubdomain == "lrsv" and "voteEvents" or "events"
      local remotePayload = {}
      remotePayload[tableField] = {}
      if type(eventPayload) == "table" then
        remotePayload[tableField][resolvedEventId] = copy(eventPayload)
      end
      prebuiltRowsBySubdomain = {
        [resolvedSubdomain] = {
          {
            id = resolvedSubdomain .. ":" .. resolvedEventId,
            subdomain = resolvedSubdomain,
            applySpec = {
              kind = "keyed_entry",
              tableField = tableField,
              key = resolvedEventId,
            },
            remotePayload = remotePayload,
          },
        },
      }
      requestedSubdomains[1] = resolvedSubdomain
    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

  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 _, event in pairs(type(stats.events) == "table" and stats.events or {}) do
      if type(event) == "table" 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 _, event in pairs(type(stats.voteEvents) == "table" and stats.voteEvents or {}) do
      if type(event) == "table" 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
    stats.schemaVersion = ROLL_STATS_SCHEMA_VERSION
    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
    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 = {
      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)
    if type(TouchLootProfile) == "function" then
      TouchLootProfile(profile)
    end
    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

    local stats = EnsureLootRollStats(profile)
    local targetKey = trim(event.target or event.targetKey)
    if targetKey == "" then
      stats.voteEvents[eventId] = nil
    else
      stats.voteEvents[eventId] = {
        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)
    if type(TouchLootProfile) == "function" then
      TouchLootProfile(profile)
    end
    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
      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 FindTopLedgerRow(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[1], candidates[2], #candidates
  end

  local function BuildNoWinnerAward(key, title, description)
    return {
      key = key,
      title = title,
      winner = "No winner yet",
      metric = "-",
      detail = description,
      hasWinner = false,
    }
  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 luckRows = BuildDiceCandidates(stats, "DESC")
    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 = string.format(
          "%d roll(s), highest %s, distance to next %.1f.",
          winner.count,
          winner.highest and tostring(math.floor(winner.highest + 0.5)) or "-",
          tonumber(distance) or 0
        ),
        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")
    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 = string.format(
          "%d roll(s), lowest %s, distance to next %.1f.",
          winner.count,
          winner.lowest and tostring(math.floor(winner.lowest + 0.5)) or "-",
          tonumber(distance) or 0
        ),
        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)
    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 = string.format("%d recorded choice(s), lead over next %d.", tonumber(winner.selectionCount) or 0, distance),
        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)
    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 = string.format("Only one vote per dropped item counts; lead over next %d vote(s).", distance),
        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 transmogWinner, transmogNext = FindTopLedgerRow(ledgerRows, "transmogCount", function(row)
      return (tonumber(row.transmogCount) or 0) > 0
    end)
    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 = string.format("Lead over next %d.", distance),
        hasWinner = true,
      }
    else
      awards[#awards + 1] = BuildNoWinnerAward("prettiest", "Prettiest Raider", "The award goes to the player who received the most transmog items.")
    end

    local disenchantWinner, disenchantNext = FindTopLedgerRow(ledgerRows, "disenchantCount", function(row)
      return (tonumber(row.disenchantCount) or 0) > 0
    end)
    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 = string.format("Lead over next %d disenchant item(s).", distance),
        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)
    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 = string.format("%d qualifying raider(s), lead over next %d need roll(s).", #forgottenRows, distance),
        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)
    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 = string.format("Lowest received items breaks ties; lead over next %d pass roll(s).", distance),
        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 hoarderWinner, hoarderNext = FindTopLedgerRow(ledgerRows, "itemCount", function(row)
      return (tonumber(row.itemCount) or 0) > 0
    end)
    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 = string.format("Lead over next %d item(s).", distance),
        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),
    }
  end

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