local API = rawget(_G, "LIDCOUNCIL_API")
local INTERNAL_API = type(API) == "table" and type(API.__internal) == "table" and API.__internal or nil
if type(INTERNAL_API) ~= "table" then
  error("LidCouncil missing internal API registry.")
end

INTERNAL_API.LIDCOUNCIL_CreateLootBoardRowHelpers_Impl = function(deps)
  deps = type(deps) == "table" and deps or {}

  local TrimText = deps.TrimText
  local GetEntryVoteTargetForVoter = deps.GetEntryVoteTargetForVoter
  local GetEntryVoteVoterKeys = deps.GetEntryVoteVoterKeys
  local GetEntryVoteCount = deps.GetEntryVoteCount
  local GetSelfKey = deps.GetSelfKey
  local GetBoardPreviewState = deps.GetBoardPreviewState
  local PersistBoardState = deps.PersistBoardState
  local GetActiveLootSession = deps.GetActiveLootSession
  local EnsureRuntimeSession = deps.EnsureRuntimeSession
  local RuntimeState = deps.RuntimeState
  local SetEntryVoteTarget = deps.SetEntryVoteTarget
  local GetBroadcastDistribution = deps.GetBroadcastDistribution
  local SendSessionComm = deps.SendSessionComm
  local NormalizeSelectionNote = deps.NormalizeSelectionNote
  local BuildMemberDisplayText = deps.BuildMemberDisplayText
  local FormatSelectionLabel = deps.FormatSelectionLabel
  local GetLootProfileAccessInfo = deps.GetLootProfileAccessInfo
  local NOTE_ICON_TEXTURE = deps.NOTE_ICON_TEXTURE
  local ShowSelectionNotePopup = deps.ShowSelectionNotePopup
  local SetBoardSelectedMember = deps.SetBoardSelectedMember
  local GetShowSelectionNotePopup = deps.GetShowSelectionNotePopup
  local GetSetBoardSelectedMember = deps.GetSetBoardSelectedMember
  local ApplyBoardButtonStyle = deps.ApplyBoardButtonStyle
  local ResolveRollOptions = deps.ResolveRollOptions
  local RecordLootVoteStat = deps.RecordLootVoteStat
  local IsRealSimulationItemKey = deps.IsRealSimulationItemKey
  local IsSelfHost = deps.IsSelfHost
  local Print = deps.Print
  local RefreshBoardAndEditor = deps.RefreshBoardAndEditor
  local BuildBoardMemberNameText
  local ProcessPendingVoteReceipts
  local MAX_VOTE_RECEIPT_ATTEMPTS = 4
  local VOTE_RECEIPT_RETRY_INTERVALS = { 1.5, 3, 6, 8 }
  local voteReceiptRetryFrame
  local OPTIONAL_MEMBER_COLUMNS = {
    { key = "total", field = "totalText", metric = "totalPoints" },
    { key = "bis", field = "bisText", metric = "bisCount" },
    { key = "offspec", field = "offSpecText", metric = "offSpecCount" },
    { key = "transmog", field = "transmogText", metric = "transmogCount" },
    { key = "disenchant", field = "disenchantText", metric = "disenchantCount" },
    { key = "items", field = "itemsText", metric = "itemCount" },
  }
  local OPTIONAL_MEMBER_COLUMN_KEYS = {}
  for _, column in ipairs(OPTIONAL_MEMBER_COLUMNS) do
    OPTIONAL_MEMBER_COLUMN_KEYS[column.key] = true
  end

  local function GetSessionCommProfileName(activeSession, fallbackProfileName)
    return TrimText(type(activeSession) == "table" and activeSession.remoteProfileName) ~= ""
      and TrimText(activeSession.remoteProfileName)
      or TrimText(fallbackProfileName)
  end

  local function GetVoteReceiptClock()
    if type(GetTime) == "function" then
      return tonumber(GetTime()) or 0
    end
    return tonumber(time and time() or 0) or 0
  end

  local function BuildVoteReceiptKey(sessionId, itemKey, voterKey, voteSeq)
    return string.lower(TrimText(sessionId))
      .. "\031" .. string.lower(TrimText(itemKey))
      .. "\031" .. string.lower(TrimText(voterKey))
      .. "\031" .. tostring(math.max(0, math.floor(tonumber(voteSeq) or 0)))
  end

  local function SetVoteReceiptRetryActive(active)
    if not voteReceiptRetryFrame and type(CreateFrame) == "function" then
      voteReceiptRetryFrame = CreateFrame("Frame")
      voteReceiptRetryFrame:Hide()
      voteReceiptRetryFrame:SetScript("OnUpdate", function(frame, elapsed)
        frame.__voteReceiptElapsed = (tonumber(frame.__voteReceiptElapsed) or 0) + (tonumber(elapsed) or 0)
        if frame.__voteReceiptElapsed < 0.25 then
          return
        end
        frame.__voteReceiptElapsed = 0
        if type(ProcessPendingVoteReceipts) == "function" then
          ProcessPendingVoteReceipts()
        end
      end)
    end
    if voteReceiptRetryFrame then
      voteReceiptRetryFrame.__voteReceiptElapsed = 0
      voteReceiptRetryFrame:SetShown(active == true)
    end
  end

  local function HasPendingVoteReceiptForItem(activeSession, itemKey, voterKey)
    if type(activeSession) ~= "table" or type(RuntimeState.pendingVoteReceipts) ~= "table" then
      return false
    end
    local sessionId = TrimText(activeSession.id)
    local resolvedItemKey = TrimText(itemKey)
    local resolvedVoterKey = string.lower(TrimText(voterKey))
    for _, receipt in pairs(RuntimeState.pendingVoteReceipts) do
      if type(receipt) == "table"
        and receipt.state == "waiting"
        and TrimText(receipt.sessionId) == sessionId
        and TrimText(receipt.itemKey) == resolvedItemKey
        and string.lower(TrimText(receipt.voterKey)) == resolvedVoterKey
      then
        return true
      end
    end
    return false
  end

  local function SendPendingVoteReceipt(receipt)
    if type(receipt) ~= "table" then
      return false, "missing_receipt"
    end
    local now = GetVoteReceiptClock()
    local sent = false
    local reason = "send_unavailable"
    if type(SendSessionComm) == "function" then
      sent, reason = SendSessionComm(
        "VOTE",
        "WHISPER",
        receipt.host,
        receipt.sessionId,
        receipt.profileName,
        receipt.itemKey,
        receipt.voterKey,
        receipt.targetKey,
        receipt.changedAt,
        receipt.voteSeq
      )
    end
    receipt.attempts = math.max(0, tonumber(receipt.attempts) or 0) + 1
    receipt.lastAttemptAt = now
    receipt.lastError = sent and "" or TrimText(reason)
    receipt.state = "waiting"
    local intervalIndex = math.min(#VOTE_RECEIPT_RETRY_INTERVALS, receipt.attempts)
    receipt.nextRetryAt = now + (tonumber(VOTE_RECEIPT_RETRY_INTERVALS[intervalIndex]) or 8)
    return sent == true, reason
  end

  ProcessPendingVoteReceipts = function()
    local receipts = type(RuntimeState.pendingVoteReceipts) == "table" and RuntimeState.pendingVoteReceipts or nil
    if not receipts or not next(receipts) then
      SetVoteReceiptRetryActive(false)
      return
    end
    local activeSession = GetActiveLootSession()
    if type(activeSession) ~= "table" or (type(IsSelfHost) == "function" and IsSelfHost(activeSession)) then
      RuntimeState.pendingVoteReceipts = {}
      SetVoteReceiptRetryActive(false)
      return
    end

    local now = GetVoteReceiptClock()
    local hasWaitingReceipt = false
    local stateChanged = false
    for receiptKey, receipt in pairs(receipts) do
      if type(receipt) == "table" then
        local sessionMatches = TrimText(receipt.sessionId) == TrimText(activeSession.id)
          and string.lower(TrimText(receipt.host)) == string.lower(TrimText(activeSession.host))
        if not sessionMatches then
          receipt.state = "superseded"
          receipts[receiptKey] = nil
        elseif receipt.state == "waiting" and now >= (tonumber(receipt.nextRetryAt) or 0) then
          if (tonumber(receipt.attempts) or 0) >= MAX_VOTE_RECEIPT_ATTEMPTS then
            receipt.state = "unconfirmed"
            stateChanged = true
            if not receipt.failureNotified and type(Print) == "function" then
              Print("Council vote was not confirmed by the host. It remains recorded locally for reconciliation.")
              receipt.failureNotified = true
            end
          else
            SendPendingVoteReceipt(receipt)
          end
        end
        if receipt.state == "waiting" then
          hasWaitingReceipt = true
        end
      end
    end
    SetVoteReceiptRetryActive(hasWaitingReceipt)
    if stateChanged and type(RefreshBoardAndEditor) == "function" then
      RefreshBoardAndEditor({ reason = "vote_receipt" })
    end
  end

  local function QueuePendingVoteReceipt(activeSession, profileName, itemKey, voterKey, targetKey, changedAt, voteSeq)
    RuntimeState.pendingVoteReceipts = type(RuntimeState.pendingVoteReceipts) == "table"
      and RuntimeState.pendingVoteReceipts
      or {}
    local receipt = {
      sessionId = TrimText(activeSession.id),
      profileName = GetSessionCommProfileName(activeSession, profileName),
      host = TrimText(activeSession.host),
      itemKey = TrimText(itemKey),
      voterKey = TrimText(voterKey),
      targetKey = TrimText(targetKey),
      changedAt = tonumber(changedAt) or 0,
      voteSeq = math.max(1, math.floor(tonumber(voteSeq) or 1)),
      attempts = 0,
      state = "waiting",
      createdAt = GetVoteReceiptClock(),
      nextRetryAt = GetVoteReceiptClock(),
    }
    RuntimeState.pendingVoteReceipts[BuildVoteReceiptKey(receipt.sessionId, receipt.itemKey, receipt.voterKey, receipt.voteSeq)] = receipt
    SendPendingVoteReceipt(receipt)
    SetVoteReceiptRetryActive(true)
  end

  local function ResolveBoardRowStateRank(selection, awardedKey, memberKey)
    local normalizedMemberKey = TrimText(memberKey)
    if TrimText(awardedKey) ~= "" and TrimText(awardedKey) == normalizedMemberKey then
      return 1
    end
    local selectionKey = string.lower(TrimText(selection and (selection.optionKey or selection.label) or "pending"))
    if selectionKey ~= "" and selectionKey ~= "pending" then
      return 2
    end
    return 3
  end

  local function ResolveSelectionConfiguredPriority(selection, rollOptions)
    if type(selection) ~= "table" or type(rollOptions) ~= "table" then
      return nil
    end
    local selectionKey = string.lower(TrimText(selection.optionKey or selection.key or selection.label))
    local selectionLabel = string.lower(TrimText(selection.label or selection.optionKey or selection.key))
    if selectionKey == "" or selectionKey == "pending" then
      return nil
    end
    for index, option in ipairs(rollOptions) do
      if type(option) == "table" then
        local optionKey = string.lower(TrimText(option.key or option.label))
        local optionLabel = string.lower(TrimText(option.label or option.key))
        if (optionKey ~= "" and (optionKey == selectionKey or optionKey == selectionLabel))
          or (optionLabel ~= "" and (optionLabel == selectionKey or optionLabel == selectionLabel)) then
          return index
        end
      end
    end
    return nil
  end

  local function ResolveBoardSortRollOptions(frame, currentEntry)
    if type(ResolveRollOptions) ~= "function" then
      return nil
    end

    local preview = GetBoardPreviewState(frame)
    if type(preview) == "table" then
      local previewProfileName = TrimText(preview.sourceProfileName)
      if previewProfileName == "" then
        previewProfileName = TrimText(preview.profileName)
      end
      return ResolveRollOptions(nil, previewProfileName)
    end

    local activeSession, activeProfileName = nil, nil
    if type(GetActiveLootSession) == "function" then
      activeSession, activeProfileName = GetActiveLootSession()
    end

    local profileName = TrimText(type(currentEntry) == "table" and currentEntry.profileName or "")
    if profileName == "" then
      profileName = TrimText(activeProfileName)
    end
    if profileName == "" and type(activeSession) == "table" then
      profileName = TrimText(activeSession.remoteProfileName)
      if profileName == "" then
        profileName = TrimText(activeSession.profileName)
      end
    end

    return ResolveRollOptions(activeSession, profileName)
  end

  local function ResolveBoardRowRollSortPriority(row, rollOptions)
    local priority = tonumber(type(row) == "table" and row.sortPriority) or nil
    if priority then
      return priority
    end
    local selection = type(row) == "table" and row.selection or nil
    local selectionKey = string.lower(TrimText(selection and (selection.optionKey or selection.label) or "pending"))
    if selectionKey == "" or selectionKey == "pending" then
      return 9999
    end
    local configuredPriority = ResolveSelectionConfiguredPriority(selection, rollOptions)
    if configuredPriority then
      return configuredPriority
    end
    if type(rollOptions) == "table" and #rollOptions > 0 then
      return #rollOptions + 1
    end
    local pointBehavior = string.upper(TrimText(selection and selection.pointBehavior or ""))
    local sectionRank = (pointBehavior == "OFFSPEC" or pointBehavior == "TRANSMOG" or pointBehavior == "NONE" or selectionKey == "greed" or selectionKey == "pass") and 2 or 1
    local fallbackPriority = 9
    if selectionKey == "need" then
      fallbackPriority = 1
    elseif selectionKey == "greed" then
      fallbackPriority = 2
    elseif selectionKey == "pass" then
      fallbackPriority = 3
    end
    return (sectionRank * 1000) + fallbackPriority
  end

  local function GetBoardMemberSortState(frame)
    local sortKey = TrimText(frame and frame.__memberSortKey or "")
    if sortKey == "" then
      sortKey = "roll"
    end
    local visibleColumns = type(frame) == "table" and type(frame.__memberColumnVisibility) == "table" and frame.__memberColumnVisibility or nil
    if OPTIONAL_MEMBER_COLUMN_KEYS[sortKey] and not (visibleColumns and visibleColumns[sortKey] == true) then
      sortKey = "roll"
      if frame then
        frame.__memberSortKey = "roll"
        frame.__memberSortDescending = false
      end
    end
    if sortKey == "roll" then
      return sortKey, false
    end
    return sortKey, frame and frame.__memberSortDescending == true
  end

  local function UpdateBoardHeaderButtonState(button, active, descending)
    if not button or not button.__labelText then
      return
    end
    local baseLabel = tostring(button.__baseLabel or "")
    if active then
      button.__labelText:SetText(baseLabel .. (descending and " ^" or " v"))
      button.__labelText:SetTextColor(1.00, 0.88, 0.32, 1.00)
    else
      button.__labelText:SetText(baseLabel)
      button.__labelText:SetTextColor(0.92, 0.92, 0.96, 1.00)
    end
  end

  local function RefreshBoardHeaderSortState(frame)
    local sortKey, descending = GetBoardMemberSortState(frame)
    for _, button in ipairs(frame and frame.__memberSortHeaders or {}) do
      UpdateBoardHeaderButtonState(button, button.__sortKey == sortKey, descending)
    end
  end

  local function SetBoardMemberSort(frame, sortKey)
    if not frame or TrimText(sortKey) == "" then
      return
    end
    local currentKey, currentDescending = GetBoardMemberSortState(frame)
    if sortKey == "roll" then
      frame.__memberSortKey = sortKey
      frame.__memberSortDescending = false
    elseif currentKey == sortKey then
      frame.__memberSortDescending = not currentDescending
    else
      frame.__memberSortKey = sortKey
      frame.__memberSortDescending = (sortKey == "dice" or sortKey == "items" or sortKey == "offspec" or sortKey == "transmog" or sortKey == "disenchant" or sortKey == "bis" or sortKey == "total" or sortKey == "votes")
    end
    RefreshBoardHeaderSortState(frame)
    if frame.RefreshBoard then
      frame:RefreshBoard()
    end
  end

  local function BuildBoardHeaderButton(frame, parent, labelText, sortKey, tooltipText, width, justify)
    local button = CreateFrame("Button", nil, parent)
    button:SetSize(width or 40, 18)
    button:RegisterForClicks("LeftButtonUp")
    button.__sortKey = TrimText(sortKey)
    button.__baseLabel = tostring(labelText or "")
    button.__tooltipText = TrimText(tooltipText)

    local text = button:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    text:SetAllPoints(true)
    text:SetJustifyH(justify or "LEFT")
    text:SetText(button.__baseLabel)
    button.__labelText = text

    if button.__sortKey ~= "" then
      button:SetScript("OnClick", function(self)
        SetBoardMemberSort(frame, self.__sortKey)
      end)
    end
    button:SetScript("OnEnter", function(self)
      if type(GameTooltip) == "table" and self.__tooltipText ~= "" then
        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        GameTooltip:SetText(self.__tooltipText, 1.00, 0.92, 0.35, 1.00, true)
        GameTooltip:Show()
      end
    end)
    button:SetScript("OnLeave", function()
      if type(GameTooltip) == "table" then
        GameTooltip:Hide()
      end
    end)

    frame.__memberSortHeaders = frame.__memberSortHeaders or {}
    frame.__memberSortHeaders[#frame.__memberSortHeaders + 1] = button
    return button
  end

  local ResolveBoardMemberMetric
  local ResolveBoardMemberTotalPoints

  local function SortBoardDisplayRows(frame, rows, totalsByCharacter, awardedKey, currentEntry)
    local sortKey, descending = GetBoardMemberSortState(frame)
    local rollOptions = ResolveBoardSortRollOptions(frame, currentEntry)
    local sortedRows = {}
    for index, row in ipairs(rows or {}) do
      sortedRows[index] = row
    end
    table.sort(sortedRows, function(left, right)
      local leftName = string.lower(tostring(left and (left.name or left.key) or ""))
      local rightName = string.lower(tostring(right and (right.name or right.key) or ""))
      if sortKey == "name" then
        if leftName == rightName then
          return ResolveBoardRowRollSortPriority(left, rollOptions) < ResolveBoardRowRollSortPriority(right, rollOptions)
        end
        if descending then
          return leftName > rightName
        end
        return leftName < rightName
      elseif sortKey == "dice" then
        local leftDice = tonumber(left and left.selection and left.selection.rollValue) or 0
        local rightDice = tonumber(right and right.selection and right.selection.rollValue) or 0
        if leftDice == rightDice then
          return ResolveBoardRowRollSortPriority(left, rollOptions) < ResolveBoardRowRollSortPriority(right, rollOptions)
        end
        if descending then
          return leftDice > rightDice
        end
        return leftDice < rightDice
      elseif sortKey == "total" then
        local leftTotal = tonumber(ResolveBoardMemberTotalPoints(totalsByCharacter, left and left.key or "")) or 0
        local rightTotal = tonumber(ResolveBoardMemberTotalPoints(totalsByCharacter, right and right.key or "")) or 0
        if leftTotal == rightTotal then
          return ResolveBoardRowRollSortPriority(left, rollOptions) < ResolveBoardRowRollSortPriority(right, rollOptions)
        end
        if descending then
          return leftTotal > rightTotal
        end
        return leftTotal < rightTotal
      elseif sortKey == "items" or sortKey == "offspec" or sortKey == "transmog" or sortKey == "disenchant" or sortKey == "bis" then
        local leftMetric = tonumber(ResolveBoardMemberMetric(totalsByCharacter, left and left.key or "", sortKey)) or 0
        local rightMetric = tonumber(ResolveBoardMemberMetric(totalsByCharacter, right and right.key or "", sortKey)) or 0
        if leftMetric == rightMetric then
          return ResolveBoardRowRollSortPriority(left, rollOptions) < ResolveBoardRowRollSortPriority(right, rollOptions)
        end
        if descending then
          return leftMetric > rightMetric
        end
        return leftMetric < rightMetric
      elseif sortKey == "state" then
        local leftRank = ResolveBoardRowStateRank(left and left.selection or nil, awardedKey, left and left.key or "")
        local rightRank = ResolveBoardRowStateRank(right and right.selection or nil, awardedKey, right and right.key or "")
        if leftRank == rightRank then
          return ResolveBoardRowRollSortPriority(left, rollOptions) < ResolveBoardRowRollSortPriority(right, rollOptions)
        end
        if descending then
          return leftRank > rightRank
        end
        return leftRank < rightRank
      elseif sortKey == "votes" then
        local leftVotes = GetEntryVoteCount(currentEntry, left and left.key or "")
        local rightVotes = GetEntryVoteCount(currentEntry, right and right.key or "")
        if leftVotes == rightVotes then
          return ResolveBoardRowRollSortPriority(left, rollOptions) < ResolveBoardRowRollSortPriority(right, rollOptions)
        end
        if descending then
          return leftVotes > rightVotes
        end
        return leftVotes < rightVotes
      else
        local leftPriority = ResolveBoardRowRollSortPriority(left, rollOptions)
        local rightPriority = ResolveBoardRowRollSortPriority(right, rollOptions)
        if leftPriority == rightPriority then
          local leftDice = tonumber(left and left.selection and left.selection.rollValue) or 0
          local rightDice = tonumber(right and right.selection and right.selection.rollValue) or 0
          if leftDice == rightDice then
            return leftName < rightName
          end
          return leftDice > rightDice
        end
        if descending then
          return leftPriority > rightPriority
        end
        return leftPriority < rightPriority
      end
    end)
    return sortedRows
  end

  local function ResolveEntryVotedKey(entry)
    return TrimText(GetEntryVoteTargetForVoter(entry, GetSelfKey()))
  end

  local function CanSelfVotePreview(frame)
    local preview = GetBoardPreviewState(frame)
    if type(GetLootProfileAccessInfo) ~= "function" then
      return false
    end

    if preview and preview.isSimulation then
      local profileName = TrimText(preview.profileName)
      if profileName == "" then
        profileName = "Simulation"
      end
      local rootProvider = rawget(_G, "LIDCOUNCIL_RootDB")
      local root = type(rootProvider) == "function" and rootProvider() or nil
      if type(root) == "table" and (type(root.profiles) ~= "table" or type(root.profiles[profileName]) ~= "table") then
        return false
      end
      local access = GetLootProfileAccessInfo(profileName)
      return type(access) == "table" and access.canOpenLiveBoard and access.canVote and true or false
    end

    local activeSession, profileName = GetActiveLootSession()
    if not activeSession or not EnsureRuntimeSession(activeSession, profileName) then
      return false
    end
    local access = GetLootProfileAccessInfo(profileName)
    return type(access) == "table" and access.canVote and true or false
  end

  local function HideBoardVoteTooltip()
    if type(GameTooltip) == "table" then
      GameTooltip:Hide()
    end
  end

  local function ShowBoardVoteTooltip(self)
    if not self or type(GameTooltip) ~= "table" then
      return
    end

    local memberName = TrimText(self.__voteMemberName)
    local voterKeys = type(self.__voteVoters) == "table" and self.__voteVoters or {}
    local voteCount = tonumber(self.__voteCount) or #voterKeys

    GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
    GameTooltip:ClearLines()
    GameTooltip:AddLine(memberName ~= "" and (memberName .. " Votes") or "Votes", 1, 0.82, 0.28)
    if voteCount <= 0 then
      GameTooltip:AddLine("No votes recorded.", 0.82, 0.82, 0.86, true)
    else
      for _, voterKey in ipairs(voterKeys) do
        GameTooltip:AddLine(tostring(voterKey), 0.92, 0.92, 0.96, true)
      end
    end
    GameTooltip:Show()
  end

  local function ToggleBoardVoteForMember(frame, memberKey)
    local normalizedMemberKey = TrimText(memberKey)
    if not frame or normalizedMemberKey == "" then
      return false
    end

    local selfKey = GetSelfKey()
    if selfKey == "" then
      return false
    end

    local preview = GetBoardPreviewState(frame)
    if preview and preview.isSimulation then
      if not CanSelfVotePreview(frame) then
        return false
      end
      local entry = preview.items and preview.items[preview.selectedItemKey] or nil
      if not entry or entry.awardedAt then
        return false
      end
      preview.selectedMemberKey = normalizedMemberKey
      entry.voteSequenceByVoter = type(entry.voteSequenceByVoter) == "table" and entry.voteSequenceByVoter or {}
      if ResolveEntryVotedKey(entry) == normalizedMemberKey then
        SetEntryVoteTarget(entry, selfKey, nil)
      else
        SetEntryVoteTarget(entry, selfKey, normalizedMemberKey)
      end
      entry.voteSequenceByVoter[selfKey] = (tonumber(entry.voteSequenceByVoter[selfKey]) or 0) + 1
      PersistBoardState(frame, {
        itemKey = tostring(RuntimeState.selectedItemKey or ""),
        voterKey = selfKey,
      })
      if frame.RefreshBoard then
        frame:RefreshBoard()
      end
      return true
    end

    local activeSession, profileName = GetActiveLootSession()
    if not activeSession or not EnsureRuntimeSession(activeSession, profileName) then
      return false
    end
    if not CanSelfVotePreview(frame) then
      return false
    end

    if type(IsSelfHost) ~= "function" or not IsSelfHost(activeSession) then
      if HasPendingVoteReceiptForItem(activeSession, RuntimeState.selectedItemKey, selfKey) then
        return false
      end
    end

    local entry = RuntimeState.items[RuntimeState.selectedItemKey]
    if not entry or entry.awardedAt then
      return false
    end

    RuntimeState.selectedMemberKey = normalizedMemberKey
    local nextTargetKey = normalizedMemberKey
    if ResolveEntryVotedKey(entry) == normalizedMemberKey then
      SetEntryVoteTarget(entry, selfKey, nil)
      nextTargetKey = ""
    else
      SetEntryVoteTarget(entry, selfKey, normalizedMemberKey)
    end
    local voteChangedAt = (type(GetServerTime) == "function" and GetServerTime()) or (time and time() or 0)
    entry.voteUpdatedAtByVoter = type(entry.voteUpdatedAtByVoter) == "table" and entry.voteUpdatedAtByVoter or {}
    entry.voteSequenceByVoter = type(entry.voteSequenceByVoter) == "table" and entry.voteSequenceByVoter or {}
    entry.voteUpdatedAtByVoter[selfKey] = voteChangedAt
    entry.voteSequenceByVoter[selfKey] = (tonumber(entry.voteSequenceByVoter[selfKey]) or 0) + 1
    local voteSeq = tonumber(entry.voteSequenceByVoter[selfKey]) or 1
    PersistBoardState(frame, {
      itemKey = tostring(RuntimeState.selectedItemKey or ""),
      voterKey = selfKey,
    })
    local selectedItemKey = tostring(RuntimeState.selectedItemKey or "")
    local selectedEntry = type(RuntimeState.items) == "table" and RuntimeState.items[selectedItemKey] or nil
    local isRealSimulationItem = (type(selectedEntry) == "table" and selectedEntry.isSimulation == true)
      or (type(IsRealSimulationItemKey) == "function" and IsRealSimulationItemKey(selectedItemKey))
    if type(RecordLootVoteStat) == "function"
      and type(IsSelfHost) == "function"
      and IsSelfHost(activeSession)
      and not isRealSimulationItem
    then
      RecordLootVoteStat(GetSessionCommProfileName(activeSession, profileName), {
        sessionId = tostring(activeSession.id or ""),
        itemKey = selectedItemKey,
        voter = selfKey,
        target = nextTargetKey,
        votedAt = voteChangedAt,
        isSimulation = isRealSimulationItem,
      })
    end
    if type(IsSelfHost) == "function" and IsSelfHost(activeSession) then
      local distribution = GetBroadcastDistribution()
      if distribution and type(SendSessionComm) == "function" then
        -- The host's own vote is already authoritative locally; broadcast the
        -- canonical state to the rest of the council.
        SendSessionComm(
          "VOTE",
          distribution,
          nil,
          tostring(activeSession.id or ""),
          GetSessionCommProfileName(activeSession, profileName),
          tostring(RuntimeState.selectedItemKey or ""),
          selfKey,
          nextTargetKey,
          voteChangedAt,
          voteSeq
        )
      end
    else
      -- Members only send votes to the host. A targeted host mirror confirms
      -- the vote, while the host then publishes the canonical board update.
      QueuePendingVoteReceipt(
        activeSession,
        profileName,
        tostring(RuntimeState.selectedItemKey or ""),
        selfKey,
        nextTargetKey,
        voteChangedAt,
        voteSeq
      )
    end
    if frame.RefreshBoard then
      frame:RefreshBoard()
    end
    return true
  end

  local function ApplyBoardMemberRowLayout(frame, row)
    if not frame or not row then
      return
    end
    local layout = frame.__memberLayout or {}
    local nameX = tonumber(layout.nameX) or 8
    local nameWidth = tonumber(layout.nameWidth) or 240
    local rollX = tonumber(layout.rollX) or 252
    local rollWidth = tonumber(layout.rollWidth) or 110
    local diceX = tonumber(layout.diceX) or 366
    local diceWidth = tonumber(layout.diceWidth) or 36
    local diceVisible = layout.diceVisible ~= false
    local voteBtnX = tonumber(layout.voteBtnX) or 510
    local voteBtnWidth = tonumber(layout.voteBtnWidth) or 50
    local voteCountX = tonumber(layout.voteCountX) or 630
    local voteCountWidth = tonumber(layout.voteCountWidth) or 32
    local noteX = tonumber(layout.noteX) or 668
    local rowWidth = tonumber(frame.memberListContent and frame.memberListContent.GetWidth and frame.memberListContent:GetWidth()) or row:GetWidth() or 470

    row:SetWidth(rowWidth)

    row.nameText:ClearAllPoints()
    row.nameText:SetPoint("LEFT", nameX, 0)
    row.nameText:SetWidth(nameWidth)
    if row.__rowData then
      row.nameText:SetText(BuildBoardMemberNameText(row.__rowData, nameWidth))
    end
    row.rollText:ClearAllPoints()
    row.rollText:SetPoint("LEFT", rollX, 0)
    row.rollText:SetWidth(rollWidth)
    row.diceText:SetShown(diceVisible)
    if diceVisible then
      row.diceText:ClearAllPoints()
      row.diceText:SetPoint("LEFT", diceX, 0)
      row.diceText:SetWidth(diceWidth)
      row.diceText:SetJustifyH("RIGHT")
    end
    for _, column in ipairs(OPTIONAL_MEMBER_COLUMNS) do
      local text = row[column.field]
      local columnLayout = type(layout.optionalColumns) == "table" and layout.optionalColumns[column.key] or nil
      local visible = type(columnLayout) == "table" and columnLayout.visible == true
      if text then
        text:SetShown(visible)
        if visible then
          text:ClearAllPoints()
          text:SetPoint("LEFT", tonumber(columnLayout.x) or 0, 0)
          text:SetWidth(tonumber(columnLayout.width) or 34)
          text:SetJustifyH("RIGHT")
        end
      end
    end
    row.stateText:Hide()
    row.voteActionBtn:ClearAllPoints()
    row.voteActionBtn:SetPoint("LEFT", voteBtnX, 0)
    row.voteActionBtn:SetWidth(voteBtnWidth)
    row.voteCountBtn:ClearAllPoints()
    row.voteCountBtn:SetPoint("LEFT", voteCountX, 0)
    row.voteCountBtn:SetWidth(voteCountWidth)
    row.noteBtn:ClearAllPoints()
    row.noteBtn:SetPoint("LEFT", noteX, 0)
  end

  local function EnsureBoardMemberRows(frame, count)
    frame.memberRows = frame.memberRows or {}
    while #frame.memberRows < count do
      local row = CreateFrame("Frame", nil, frame.memberListContent)
      row:SetSize(470, 22)
      row:EnableMouse(true)

      row.bg = row:CreateTexture(nil, "BACKGROUND")
      row.bg:SetAllPoints(true)

      row.highlight = row:CreateTexture(nil, "ARTWORK")
      row.highlight:SetAllPoints(true)
      row.highlight:SetColorTexture(0.18, 0.56, 0.96, 0.34)
      row.highlight:Hide()

      row.hover = row:CreateTexture(nil, "ARTWORK")
      row.hover:SetAllPoints(true)
      row.hover:SetColorTexture(0.26, 0.52, 0.84, 0.18)
      row.hover:Hide()

      row.nameText = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
      row.nameText:SetPoint("LEFT", 8, 0)
      row.nameText:SetWidth(240)
      row.nameText:SetJustifyH("LEFT")
      if row.nameText.SetWordWrap then row.nameText:SetWordWrap(false) end
      if row.nameText.SetNonSpaceWrap then row.nameText:SetNonSpaceWrap(false) end
      if row.nameText.SetMaxLines then row.nameText:SetMaxLines(1) end

      row.rollText = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
      row.rollText:SetPoint("LEFT", 252, 0)
      row.rollText:SetWidth(110)
      row.rollText:SetJustifyH("LEFT")
      if row.rollText.SetWordWrap then row.rollText:SetWordWrap(false) end
      if row.rollText.SetNonSpaceWrap then row.rollText:SetNonSpaceWrap(false) end
      if row.rollText.SetMaxLines then row.rollText:SetMaxLines(1) end

      row.diceText = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
      row.diceText:SetPoint("LEFT", 366, 0)
      row.diceText:SetWidth(36)
      row.diceText:SetJustifyH("RIGHT")

      for _, column in ipairs(OPTIONAL_MEMBER_COLUMNS) do
        local metricText = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        metricText:SetPoint("LEFT", 410, 0)
        metricText:SetWidth(34)
        metricText:SetJustifyH("RIGHT")
        metricText:Hide()
        row[column.field] = metricText
      end

      row.stateText = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
      row.stateText:SetPoint("LEFT", 510, 0)
      row.stateText:SetWidth(62)
      row.stateText:SetJustifyH("LEFT")

      local voteActionBtn = CreateFrame("Button", nil, row, "UIPanelButtonTemplate")
      voteActionBtn:SetSize(50, 18)
      voteActionBtn:SetPoint("LEFT", 576, 0)
      voteActionBtn:SetText("Vote")
      ApplyBoardButtonStyle(voteActionBtn)
      voteActionBtn:SetScript("OnClick", function(self)
        ToggleBoardVoteForMember(frame, self.__memberKey)
      end)
      voteActionBtn:SetScript("OnEnter", function(self)
        if type(GameTooltip) ~= "table" then
          return
        end
        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        if self.__voteAwaitingReceipt then
          GameTooltip:SetText("Waiting for Host", 1.00, 0.74, 0.28, 1.00)
          GameTooltip:AddLine("Your vote was sent and is waiting for the host's confirmation.", 0.92, 0.92, 0.96, true)
        elseif self.__hasLocalVote then
          GameTooltip:SetText("Your Vote", 0.50, 0.82, 1.00, 1.00)
          GameTooltip:AddLine("Your vote is recorded for this candidate. Click to remove it.", 0.92, 0.92, 0.96, true)
        else
          GameTooltip:SetText("Vote", 1.00, 0.82, 0.28, 1.00)
          GameTooltip:AddLine("Record your vote for this candidate. Choosing another candidate moves your vote.", 0.92, 0.92, 0.96, true)
        end
        GameTooltip:Show()
      end)
      voteActionBtn:SetScript("OnLeave", HideBoardVoteTooltip)
      row.voteActionBtn = voteActionBtn

      local voteCountBtn = CreateFrame("Button", nil, row)
      voteCountBtn:SetSize(32, 18)
      voteCountBtn:SetPoint("LEFT", 630, 0)
      voteCountBtn:SetHighlightTexture("Interface\\Buttons\\WHITE8x8")
      if voteCountBtn.GetHighlightTexture then
        local highlight = voteCountBtn:GetHighlightTexture()
        if highlight then
          highlight:SetVertexColor(0.90, 0.74, 0.18, 0.12)
        end
      end
      voteCountBtn.text = voteCountBtn:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
      voteCountBtn.text:SetAllPoints(true)
      voteCountBtn.text:SetJustifyH("RIGHT")
      voteCountBtn:SetScript("OnEnter", ShowBoardVoteTooltip)
      voteCountBtn:SetScript("OnLeave", HideBoardVoteTooltip)
      row.voteCountBtn = voteCountBtn

      local noteBtn = CreateFrame("Button", nil, row)
      noteBtn:SetSize(16, 16)
      noteBtn:SetPoint("LEFT", 668, 0)
      local noteIcon = noteBtn:CreateTexture(nil, "ARTWORK")
      noteIcon:SetAllPoints(true)
      noteIcon:SetTexture(NOTE_ICON_TEXTURE)
      noteBtn.icon = noteIcon
      noteBtn:SetScript("OnClick", function(self)
        local showNotePopup = type(GetShowSelectionNotePopup) == "function" and GetShowSelectionNotePopup() or ShowSelectionNotePopup
        if type(showNotePopup) == "function" then
          showNotePopup(self.__memberName, self.__selection)
        end
      end)
      noteBtn:Hide()
      row.noteBtn = noteBtn

      row:SetScript("OnMouseUp", function(self, button)
        if button ~= "LeftButton" or TrimText(self.__memberKey) == "" then
          return
        end
        local setSelectedMember = type(GetSetBoardSelectedMember) == "function" and GetSetBoardSelectedMember() or SetBoardSelectedMember
        if type(setSelectedMember) == "function" then
          setSelectedMember(frame, self.__memberKey)
        end
      end)
      row:SetScript("OnEnter", function(self)
        if not self.__selected then
          self.hover:Show()
        end
      end)
      row:SetScript("OnLeave", function(self)
        self.hover:Hide()
      end)

      frame.memberRows[#frame.memberRows + 1] = row
    end
  end

  ResolveBoardMemberMetric = function(totalsByCharacter, memberKey, metricKey)
    if type(totalsByCharacter) ~= "table" then
      return nil
    end
    local resolvedMemberKey = TrimText(memberKey)
    local normalizedMemberKey = string.lower(resolvedMemberKey:gsub("%s*%-%s*", "-"):gsub("%s+", ""))
    local totalEntry = totalsByCharacter[memberKey]
      or totalsByCharacter[resolvedMemberKey]
      or totalsByCharacter[normalizedMemberKey]
    if type(totalEntry) == "table" then
      local key = TrimText(metricKey)
      if key == "total" then
        return tonumber(totalEntry.totalPoints) or tonumber(totalEntry.ownPoints)
      elseif key == "items" then
        return tonumber(totalEntry.itemCount)
      elseif key == "offspec" then
        return tonumber(totalEntry.offSpecCount)
      elseif key == "transmog" then
        return tonumber(totalEntry.transmogCount)
      elseif key == "disenchant" then
        return tonumber(totalEntry.disenchantCount)
      elseif key == "bis" then
        return tonumber(totalEntry.bisCount)
      end
      return tonumber(totalEntry[key])
    end
    return tonumber(totalEntry)
  end

  ResolveBoardMemberTotalPoints = function(totalsByCharacter, memberKey)
    return ResolveBoardMemberMetric(totalsByCharacter, memberKey, "total")
  end

  local function ResolveBoardSelectionColor(selection)
    local color = type(selection) == "table" and selection.color or nil
    return tonumber(color and color[1]) or 0.82,
      tonumber(color and color[2]) or 0.82,
      tonumber(color and color[3]) or 0.86
  end

  local function ResolveBoardSelectionDiceText(selection)
    local rollValue = tonumber(selection and selection.rollValue)
    if rollValue and rollValue > 0 then
      return tostring(math.floor(rollValue + 0.5))
    end
    return "-"
  end

  local function SplitLeadingIconMarkup(text)
    local resolved = tostring(text or "")
    local iconMarkup = resolved:match("^(|T.-|t%s*)")
    if iconMarkup and iconMarkup ~= "" then
      return iconMarkup, resolved:sub(#iconMarkup + 1)
    end
    return "", resolved
  end

  local function TruncateBoardText(text, width)
    local resolved = tostring(text or "")
    local maxChars = math.max(8, math.floor((tonumber(width) or 120) / 6.2))
    if #resolved <= maxChars then
      return resolved
    end
    return resolved:sub(1, math.max(1, maxChars - 4)) .. "...."
  end

  BuildBoardMemberNameText = function(rowData, width)
    local fullText = BuildMemberDisplayText(rowData, { iconSize = 14 })
    local iconMarkup, labelText = SplitLeadingIconMarkup(fullText)
    local labelWidth = math.max(24, (tonumber(width) or 120) - (iconMarkup ~= "" and 22 or 0))
    return iconMarkup .. TruncateBoardText(labelText, labelWidth)
  end

  local function ResolveBoardMemberState(selection, memberKey, votedKey, awardedKey)
    local normalizedMemberKey = TrimText(memberKey)
    if TrimText(awardedKey) ~= "" and TrimText(awardedKey) == normalizedMemberKey then
      return "Assigned", 0.22, 0.88, 0.32
    end

    local selectionKey = string.lower(TrimText(selection and (selection.optionKey or selection.label) or "pending"))
    if selectionKey ~= "" and selectionKey ~= "pending" then
      local r, g, b = ResolveBoardSelectionColor(selection)
      return "Rolled", r, g, b
    end
    return "Pending", 0.82, 0.82, 0.86
  end

  local function UpdateBoardMemberRows(frame, rows, selectedKey, totalsByCharacter, votedKey, awardedKey, currentEntry)
    rows = SortBoardDisplayRows(frame, rows or {}, totalsByCharacter, awardedKey, currentEntry)
    EnsureBoardMemberRows(frame, #rows)

    local activeSession = GetActiveLootSession()
    local voteAwaitingReceipt = type(activeSession) == "table"
      and (type(IsSelfHost) ~= "function" or not IsSelfHost(activeSession))
      and HasPendingVoteReceiptForItem(activeSession, RuntimeState.selectedItemKey, GetSelfKey())

    local rowCount = #rows
    for index, viewRow in ipairs(frame.memberRows or {}) do
      local rowData = rows[index]
      if rowData then
        viewRow.__rowData = rowData
        local selection = rowData.selection
        local layout = frame.__memberLayout or {}
        local rowLabel = BuildBoardMemberNameText(rowData, tonumber(layout.nameWidth) or 240)
        local rollLabel = FormatSelectionLabel(selection)
        local diceLabel = ResolveBoardSelectionDiceText(selection)
        local stateText, stateR, stateG, stateB = ResolveBoardMemberState(selection, rowData.key, votedKey, awardedKey)
        local rollR, rollG, rollB = ResolveBoardSelectionColor(selection)
        local voteCount = GetEntryVoteCount(currentEntry, rowData.key)
        local voteVoters = GetEntryVoteVoterKeys(currentEntry, rowData.key)
        local hasLocalVote = ResolveEntryVotedKey(currentEntry) == TrimText(rowData.key)

        viewRow.__memberKey = rowData.key
        viewRow:ClearAllPoints()
        viewRow:SetPoint("TOPLEFT", 0, -((index - 1) * 24))
        viewRow:SetSize((frame.memberListContent and frame.memberListContent:GetWidth()) or 470, 22)
        viewRow:Show()

        viewRow.nameText:SetText(rowLabel)
        viewRow.rollText:SetText(rollLabel)
        viewRow.rollText:SetTextColor(rollR, rollG, rollB, 1)
        viewRow.diceText:SetText(diceLabel)
        viewRow.diceText:SetTextColor(rollR, rollG, rollB, 1)
        for _, column in ipairs(OPTIONAL_MEMBER_COLUMNS) do
          local text = viewRow[column.field]
          if text then
            local value = ResolveBoardMemberMetric(totalsByCharacter, rowData.key, column.key)
            text:SetText(value and tostring(value) or "-")
            if column.key == "total" or column.key == "bis" then
              text:SetTextColor(1.00, 0.88, 0.32, 1)
            else
              text:SetTextColor(0.86, 0.88, 0.94, 1)
            end
          end
        end
        viewRow.stateText:SetText(stateText)
        viewRow.stateText:SetTextColor(stateR, stateG, stateB, 1)
        viewRow.voteActionBtn.__memberKey = rowData.key
        viewRow.voteActionBtn.__hasLocalVote = hasLocalVote
        viewRow.voteActionBtn.__voteAwaitingReceipt = voteAwaitingReceipt
        viewRow.voteActionBtn.__lidBoardSelected = hasLocalVote
        viewRow.voteActionBtn:SetText(voteAwaitingReceipt and "Wait" or (hasLocalVote and "Unvote" or "Vote"))
        if currentEntry and currentEntry.awardedAt then
          viewRow.voteActionBtn:Disable()
        elseif voteAwaitingReceipt then
          viewRow.voteActionBtn:Disable()
        elseif not CanSelfVotePreview(frame) then
          viewRow.voteActionBtn:Disable()
        else
          viewRow.voteActionBtn:Enable()
        end
        ApplyBoardButtonStyle(viewRow.voteActionBtn)
        viewRow.voteCountBtn.__voteCount = voteCount
        viewRow.voteCountBtn.__voteVoters = voteVoters
        viewRow.voteCountBtn.__voteMemberName = tostring(rowData.name or rowData.key or "?")
        viewRow.voteCountBtn.text:SetText(tostring(voteCount or 0))
        if voteCount and voteCount > 0 then
          viewRow.voteCountBtn.text:SetTextColor(1, 0.82, 0.28, 1)
        else
          viewRow.voteCountBtn.text:SetTextColor(0.72, 0.72, 0.76, 1)
        end

        local note = NormalizeSelectionNote(selection and selection.note or "")
        viewRow.noteBtn.__selection = selection
        viewRow.noteBtn.__noteText = note
        viewRow.noteBtn.__memberName = tostring(rowData.name or rowData.key or "?")
        if note ~= "" then
          viewRow.noteBtn:Show()
        else
          viewRow.noteBtn:Hide()
        end

        local isSelected = TrimText(selectedKey) ~= "" and TrimText(selectedKey) == TrimText(rowData.key)
        viewRow.__selected = isSelected
        viewRow.highlight:SetShown(isSelected)
        viewRow.hover:Hide()
        if isSelected then
          viewRow.bg:SetColorTexture(0.10, 0.20, 0.34, 0.82)
        elseif (index % 2) == 0 then
          viewRow.bg:SetColorTexture(0.08, 0.08, 0.10, 0.46)
        else
          viewRow.bg:SetColorTexture(0.05, 0.05, 0.07, 0.32)
        end
        ApplyBoardMemberRowLayout(frame, viewRow)
        viewRow:Show()
      else
        viewRow.__rowData = nil
        viewRow.__selected = false
        viewRow:Hide()
        viewRow.__memberKey = nil
        viewRow.voteActionBtn.__memberKey = nil
        viewRow.voteActionBtn.__hasLocalVote = false
        viewRow.voteActionBtn.__voteAwaitingReceipt = false
        viewRow.voteActionBtn.__lidBoardSelected = false
        ApplyBoardButtonStyle(viewRow.voteActionBtn)
        viewRow.noteBtn:Hide()
        viewRow.highlight:Hide()
        viewRow.hover:Hide()
      end
    end

    if frame.memberListContent then
      frame.memberListContent:SetHeight(math.max(1, rowCount * 24))
    end
  end

  return {
    RefreshBoardHeaderSortState = RefreshBoardHeaderSortState,
    BuildBoardHeaderButton = BuildBoardHeaderButton,
    ResolveEntryVotedKey = ResolveEntryVotedKey,
    ToggleBoardVoteForMember = ToggleBoardVoteForMember,
    ApplyBoardMemberRowLayout = ApplyBoardMemberRowLayout,
    EnsureBoardMemberRows = EnsureBoardMemberRows,
    ResolveBoardMemberTotalPoints = ResolveBoardMemberTotalPoints,
    UpdateBoardMemberRows = UpdateBoardMemberRows,
  }
end
