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 IsSelfHost = deps.IsSelfHost
  local BuildBoardMemberNameText
  local OPTIONAL_MEMBER_COLUMNS = {
    { key = "items", field = "itemsText", metric = "itemCount" },
    { key = "offspec", field = "offSpecText", metric = "offSpecCount" },
    { key = "transmog", field = "transmogText", metric = "transmogCount" },
    { key = "disenchant", field = "disenchantText", metric = "disenchantCount" },
    { key = "total", field = "totalText", metric = "totalPoints" },
    { key = "bis", field = "bisText", metric = "bisCount" },
  }
  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 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

    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,
    })
    if type(RecordLootVoteStat) == "function" and type(IsSelfHost) == "function" and IsSelfHost(activeSession) then
      RecordLootVoteStat(GetSessionCommProfileName(activeSession, profileName), {
        sessionId = tostring(activeSession.id or ""),
        itemKey = tostring(RuntimeState.selectedItemKey or ""),
        voter = selfKey,
        target = nextTargetKey,
        votedAt = voteChangedAt,
      })
    end
    local distribution = GetBroadcastDistribution()
    if distribution and type(SendSessionComm) == "function" then
      SendSessionComm(
        "VOTE",
        distribution,
        nil,
        tostring(activeSession.id or ""),
        GetSessionCommProfileName(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 stateX = tonumber(layout.stateX) or 510
    local stateWidth = tonumber(layout.stateWidth) or 62
    local voteBtnX = tonumber(layout.voteBtnX) or 576
    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:ClearAllPoints()
    row.diceText:SetPoint("LEFT", diceX, 0)
    row.diceText:SetWidth(diceWidth)
    row.diceText:SetJustifyH("RIGHT")
    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:ClearAllPoints()
    row.stateText:SetPoint("LEFT", stateX, 0)
    row.stateText:SetWidth(stateWidth)
    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.74, 0.22, 0.30, 0.18)
      row.highlight: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)
      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)

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

  ResolveBoardMemberMetric = function(totalsByCharacter, memberKey, metricKey)
    if type(totalsByCharacter) ~= "table" then
      return nil
    end
    local totalEntry = totalsByCharacter[memberKey] or totalsByCharacter[TrimText(memberKey)]
    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 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:SetText(hasLocalVote and "Unvote" or "Vote")
        if currentEntry and currentEntry.awardedAt then
          viewRow.voteActionBtn:Disable()
        elseif not CanSelfVotePreview(frame) then
          viewRow.voteActionBtn:Disable()
        else
          viewRow.voteActionBtn:Enable()
        end
        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

        viewRow.highlight:SetShown(TrimText(selectedKey) ~= "" and TrimText(selectedKey) == TrimText(rowData.key))
        if (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:Hide()
        viewRow.__memberKey = nil
        viewRow.voteActionBtn.__memberKey = nil
        viewRow.noteBtn:Hide()
        viewRow.highlight: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
