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_CreateLootBoardActionHelpers_Impl = function(deps)
  deps = type(deps) == "table" and deps or {}

  local ApplyFrameSkin = deps.ApplyFrameSkin
  local ApplyBoardButtonStyle = deps.ApplyBoardButtonStyle
  local BuildMemberDisplayText = deps.BuildMemberDisplayText
  local GetActiveLootSession = deps.GetActiveLootSession
  local IsSelfHost = deps.IsSelfHost
  local CanSelfAssignLiveBoard = deps.CanSelfAssignLiveBoard
  local EnsureRuntimeSession = deps.EnsureRuntimeSession
  local ResolveRollOptions = deps.ResolveRollOptions
  local RecordChoice = deps.RecordChoice
  local SendSessionComm = deps.SendSessionComm
  local GetBroadcastDistribution = deps.GetBroadcastDistribution
  local GetSessionCommProfileName = deps.GetSessionCommProfileName
  local ResolveSessionUnit = deps.ResolveSessionUnit
  local RefreshBoardAndEditor = deps.RefreshBoardAndEditor
  local PersistRuntimeBoardHistory = deps.PersistRuntimeBoardHistory
  local RuntimeState = deps.RuntimeState
  local TrimText = deps.TrimText
  local ResolveOptionIconTexture = deps.ResolveOptionIconTexture
  local GetSelfKey = deps.GetSelfKey
  local LIVE_BOARD_ASSIGNMENT_FIELD_KEYS = {
    "awardedTo",
    "awardedPoints",
    "awardedNote",
    "awardedAt",
    "ledgerEntryId",
    "awardUpdatedAt",
    "tradeState",
    "tradedAt",
    "tradedBy",
    "tradeUpdatedAt",
  }

  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 now()
    return (type(GetServerTime) == "function" and GetServerTime()) or (time and time() or 0)
  end

  local function GetActiveSessionContext()
    if type(GetActiveLootSession) ~= "function" then
      return nil, nil
    end
    return GetActiveLootSession()
  end

  local function EnsureActionPopupFrame(owner, key, name, width, height, titleText)
    if owner[key] then
      return owner[key]
    end

    local frame = CreateFrame("Frame", name, UIParent, "BackdropTemplate")
    frame:SetSize(width, height)
    frame:SetPoint("CENTER", 0, 0)
    frame:SetFrameStrata("FULLSCREEN_DIALOG")
    frame:SetMovable(true)
    frame:EnableMouse(true)
    frame:SetToplevel(true)
    frame:RegisterForDrag("LeftButton")
    frame:SetScript("OnDragStart", frame.StartMoving)
    frame:SetScript("OnDragStop", frame.StopMovingOrSizing)
    if type(ApplyFrameSkin) == "function" then
      ApplyFrameSkin(frame)
    end

    local title = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    title:SetPoint("TOPLEFT", 16, -16)
    title:SetText(titleText)
    frame.title = title

    local closeBtn = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate")
    closeBtn:SetSize(70, 22)
    closeBtn:SetPoint("TOPRIGHT", -14, -14)
    closeBtn:SetText("Close")
    if type(ApplyBoardButtonStyle) == "function" then
      ApplyBoardButtonStyle(closeBtn)
    end
    closeBtn:SetScript("OnClick", function()
      frame:Hide()
    end)
    frame.closeBtn = closeBtn

    owner[key] = frame
    frame:Hide()
    return frame
  end

  local function ResolveEntrySelection(entry, memberKey)
    if type(entry) ~= "table" or type(entry.selections) ~= "table" then
      return nil, nil
    end
    local needle = string.lower(trim(memberKey))
    local direct = entry.selections[memberKey]
    if type(direct) == "table" then
      return direct, memberKey
    end
    for storedKey, selection in pairs(entry.selections) do
      if string.lower(trim(storedKey)) == needle and type(selection) == "table" then
        return selection, storedKey
      end
    end
    return nil, nil
  end

  local function OptionUsesDice(option)
    local behavior = string.upper(trim(type(option) == "table" and option.pointBehavior or ""))
    local key = string.lower(trim(type(option) == "table" and (option.key or option.optionKey or option.label) or ""))
    return behavior ~= "NONE" and key ~= "" and key ~= "pending" and key ~= "pass"
  end

  local function BuildOptionPayload(option, existingSelection)
    local resolved = type(option) == "table" and option or {}
    local payload = {
      key = trim(resolved.key or resolved.optionKey or resolved.label),
      label = trim(resolved.label or resolved.key or resolved.optionKey),
      color = {
        tonumber(resolved.color and resolved.color[1]) or 0.82,
        tonumber(resolved.color and resolved.color[2]) or 0.82,
        tonumber(resolved.color and resolved.color[3]) or 0.86,
      },
      icon = resolved.icon,
      pointBehavior = trim(resolved.pointBehavior),
      note = trim(type(existingSelection) == "table" and existingSelection.note or ""),
      selectedAt = now(),
      selectedSeq = (tonumber(type(existingSelection) == "table" and existingSelection.selectedSeq) or 0) + 1,
    }

    if OptionUsesDice(payload) then
      payload.rollValue = tonumber(type(existingSelection) == "table" and existingSelection.rollValue) or math.random(1, 100)
    else
      payload.rollValue = nil
    end
    return payload
  end

  local function BroadcastRollOverride(activeSession, profileName, entry, memberKey, option)
    if type(SendSessionComm) ~= "function" or type(GetBroadcastDistribution) ~= "function" then
      return
    end
    local distribution = GetBroadcastDistribution()
    if not distribution then
      return
    end
    SendSessionComm(
      "RCHOICE",
      distribution,
      nil,
      trim(type(activeSession) == "table" and activeSession.id or ""),
      type(GetSessionCommProfileName) == "function" and GetSessionCommProfileName(activeSession, profileName) or trim(profileName),
      trim(type(entry) == "table" and entry.key or ""),
      trim(memberKey),
      trim(option and option.key),
      trim(option and option.label),
      trim(option and option.note),
      option and option.rollValue or "",
      option and option.selectedAt or "",
      option and option.selectedSeq or ""
    )
  end

  local function ApplyHostRollOverride(boardFrame, entry, memberKey, option)
    local activeSession, profileName = GetActiveSessionContext()
    if type(activeSession) ~= "table" or trim(profileName) == "" then
      return false
    end
    if type(IsSelfHost) ~= "function" or not IsSelfHost(activeSession) then
      return false
    end
    if type(CanSelfAssignLiveBoard) == "function" and not CanSelfAssignLiveBoard(activeSession, profileName) then
      return false
    end
    if type(EnsureRuntimeSession) == "function" and not EnsureRuntimeSession(activeSession, profileName) then
      return false
    end
    if type(entry) ~= "table" or trim(entry.key) == "" or trim(memberKey) == "" then
      return false
    end

    local existingSelection = ResolveEntrySelection(entry, memberKey)
    local payload = BuildOptionPayload(option, existingSelection)
    if trim(payload.key) == "" and trim(payload.label) == "" then
      return false
    end

    if type(RecordChoice) == "function" then
      RecordChoice(entry, memberKey, payload, "OVERRIDE")
      local storedSelection = ResolveEntrySelection(entry, memberKey)
      if type(storedSelection) == "table" then
        payload.selectedAt = tonumber(storedSelection.selectedAt) or payload.selectedAt
        payload.selectedSeq = tonumber(storedSelection.selectedSeq) or payload.selectedSeq
        payload.rollValue = tonumber(storedSelection.rollValue) or nil
      end
    end

    BroadcastRollOverride(activeSession, profileName, entry, memberKey, payload)
    if type(RefreshBoardAndEditor) == "function" then
      RefreshBoardAndEditor({ reason = "choice", itemKey = entry.key })
    elseif boardFrame and boardFrame.RefreshBoard then
      boardFrame:RefreshBoard()
    end
    return true
  end

  local function EnsureChangeRollRows(frame, count)
    frame.optionRows = frame.optionRows or {}
    while #frame.optionRows < count do
      local button = CreateFrame("Button", nil, frame, "BackdropTemplate")
      button:SetSize(382, 42)
      if type(ApplyFrameSkin) == "function" then
        ApplyFrameSkin(button)
      end
      if button.SetBackdropColor then
        button.__normalBackdrop = { 0.07, 0.09, 0.12, 0.96 }
        button.__hoverBackdrop = { 0.12, 0.16, 0.23, 0.98 }
        button:SetBackdropColor(button.__normalBackdrop[1], button.__normalBackdrop[2], button.__normalBackdrop[3], button.__normalBackdrop[4])
      end
      button:SetScript("OnEnter", function(self)
        if self.SetBackdropColor and self.__hoverBackdrop then
          self:SetBackdropColor(self.__hoverBackdrop[1], self.__hoverBackdrop[2], self.__hoverBackdrop[3], self.__hoverBackdrop[4])
        end
      end)
      button:SetScript("OnLeave", function(self)
        if self.SetBackdropColor and self.__normalBackdrop then
          self:SetBackdropColor(self.__normalBackdrop[1], self.__normalBackdrop[2], self.__normalBackdrop[3], self.__normalBackdrop[4])
        end
      end)

      local selected = button:CreateTexture(nil, "BORDER")
      selected:SetTexture("Interface\\Buttons\\WHITE8x8")
      selected:SetPoint("TOPLEFT", 1, -1)
      selected:SetPoint("BOTTOMLEFT", 1, 1)
      selected:SetWidth(4)
      selected:SetVertexColor(1.00, 0.86, 0.28, 0)
      button.selectedAccent = selected

      local icon = button:CreateTexture(nil, "ARTWORK")
      icon:SetSize(24, 24)
      icon:SetPoint("LEFT", 10, 0)
      button.optionIcon = icon

      local swatch = button:CreateTexture(nil, "ARTWORK")
      swatch:SetTexture("Interface\\Buttons\\WHITE8x8")
      swatch:SetSize(14, 14)
      swatch:SetPoint("LEFT", icon, "RIGHT", 8, 0)
      button.colorSwatch = swatch

      local label = button:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
      label:SetPoint("TOPLEFT", swatch, "RIGHT", 8, 9)
      label:SetPoint("RIGHT", button, "RIGHT", -10, 0)
      label:SetJustifyH("LEFT")
      if label.SetWordWrap then
        label:SetWordWrap(false)
      end
      button.label = label

      local meta = button:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall")
      meta:SetPoint("TOPLEFT", label, "BOTTOMLEFT", 0, -4)
      meta:SetPoint("RIGHT", button, "RIGHT", -10, 0)
      meta:SetJustifyH("LEFT")
      if meta.SetWordWrap then
        meta:SetWordWrap(false)
      end
      button.meta = meta

      frame.optionRows[#frame.optionRows + 1] = button
    end
    return frame.optionRows
  end

  local function FormatPointBehaviorLabel(value)
    local behavior = string.upper(trim(value))
    if behavior == "" then
      return "None"
    elseif behavior == "ITEMLEVEL" then
      return "Itemlevel Based"
    elseif behavior == "OFFSPEC" then
      return "Off-Spec"
    elseif behavior == "TRANSMOG" then
      return "Transmog"
    elseif behavior == "DISENCHANT" then
      return "Disenchant"
    elseif behavior == "BIS" then
      return "BiS"
    elseif behavior == "NONE" then
      return "None"
    end
    return tostring(value)
  end

  local function FormatPopupSectionLabel(value)
    local section = string.upper(trim(value))
    if section == "GREEDPASS" then
      return "Greed / Pass"
    elseif section == "NEED" then
      return "Need"
    end
    return "Need"
  end

  local function OpenChangeRollPopup(boardFrame, entry, memberKey)
    local activeSession, profileName = GetActiveSessionContext()
    if type(activeSession) ~= "table" or type(entry) ~= "table" or trim(memberKey) == "" then
      return false
    end

    local popup = EnsureActionPopupFrame(boardFrame, "__changeRollPopup", "LIDCOUNCIL_ChangeRollPopup", 430, 440, "Change Roll")
    popup.__entry = entry
    popup.__memberKey = memberKey

    if not popup.desc then
      local desc = popup:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
      desc:SetPoint("TOPLEFT", 16, -48)
      desc:SetWidth(390)
      desc:SetJustifyH("LEFT")
      desc:SetJustifyV("TOP")
      desc:SetText("")
      popup.desc = desc
    end

    popup.desc:SetText("Select the corrected roll option for:\n" .. tostring(memberKey))

    local options = type(ResolveRollOptions) == "function" and ResolveRollOptions(activeSession, profileName) or {}
    popup:SetHeight(math.max(300, 116 + (#options * 48)))
    local rows = EnsureChangeRollRows(popup, math.max(1, #options))
    local currentSelection = ResolveEntrySelection(entry, memberKey)
    local currentKey = string.lower(trim(type(currentSelection) == "table" and (currentSelection.optionKey or currentSelection.label) or ""))
    for index, row in ipairs(rows) do
      local option = options[index]
      if type(option) == "table" then
        local label = trim(option.label or option.key)
        local key = string.lower(trim(option.key or option.label))
        local isCurrent = currentKey ~= "" and (currentKey == key or currentKey == string.lower(label))
        local color = type(option.color) == "table" and option.color or nil
        local icon = type(ResolveOptionIconTexture) == "function" and ResolveOptionIconTexture(option.icon, option.key, option.label) or option.icon
        row:ClearAllPoints()
        row:SetPoint("TOPLEFT", 24, -92 - ((index - 1) * 48))
        if row.selectedAccent then
          row.selectedAccent:SetVertexColor(1.00, 0.86, 0.28, isCurrent and 0.96 or 0)
        end
        if row.optionIcon then
          if icon ~= nil and icon ~= "" then
            row.optionIcon:SetTexture(icon)
            row.optionIcon:Show()
          else
            row.optionIcon:Hide()
          end
        end
        if row.colorSwatch then
          row.colorSwatch:SetVertexColor(
            tonumber(color and color[1]) or 0.82,
            tonumber(color and color[2]) or 0.82,
            tonumber(color and color[3]) or 0.86,
            1
          )
        end
        if row.label then
          row.label:SetText((isCurrent and "* " or "") .. "#" .. tostring(index) .. " - " .. label)
          row.label:SetTextColor(
            tonumber(color and color[1]) or 1,
            tonumber(color and color[2]) or 1,
            tonumber(color and color[3]) or 1,
            1
          )
        end
        if row.meta then
          row.meta:SetText("Point: " .. FormatPointBehaviorLabel(option.pointBehavior) .. " | Category: " .. FormatPopupSectionLabel(option.popupSection))
        end
        row:SetScript("OnClick", function()
          if ApplyHostRollOverride(boardFrame, entry, memberKey, option) then
            popup:Hide()
          end
        end)
        row:Show()
      else
        row:Hide()
      end
    end

    popup:Show()
    if popup.Raise then
      popup:Raise()
    end
    return true
  end

  local function EnsureTradeState()
    RuntimeState.tradeHelper = type(RuntimeState.tradeHelper) == "table" and RuntimeState.tradeHelper or {}
    return RuntimeState.tradeHelper
  end

  local function SetTradeState(itemKey, state, detail)
    local states = EnsureTradeState()
    local key = trim(itemKey)
    if key == "" then
      return
    end
    states[key] = {
      state = trim(state),
      detail = trim(detail),
      updatedAt = now(),
    }
  end

  local function GetTradeState(itemKey)
    local states = EnsureTradeState()
    return type(states[trim(itemKey)]) == "table" and states[trim(itemKey)] or nil
  end

  local function MarkEntryTradedAndBroadcast(boardFrame, entry)
    local activeSession, profileName = GetActiveSessionContext()
    if type(activeSession) ~= "table" or type(entry) ~= "table" or trim(entry.key) == "" then
      return false
    end
    if type(IsSelfHost) ~= "function" or not IsSelfHost(activeSession) then
      return false
    end
    if type(CanSelfAssignLiveBoard) == "function" and not CanSelfAssignLiveBoard(activeSession, profileName) then
      return false
    end
    if not entry.awardedAt or trim(entry.awardedTo) == "" then
      return false
    end

    local changedAt = now()
    local changedBy = type(GetSelfKey) == "function" and trim(GetSelfKey()) or trim(activeSession.host)
    if changedBy == "" then
      changedBy = trim(activeSession.host)
    end

    entry.tradeState = "TRADED"
    entry.tradedAt = changedAt
    entry.tradedBy = changedBy
    entry.tradeUpdatedAt = changedAt
    if type(RuntimeState) == "table" then
      RuntimeState.boardUpdatedAt = math.max(tonumber(RuntimeState.boardUpdatedAt) or 0, changedAt)
    end
    SetTradeState(entry.key, "TRADED", "Marked as traded by the session host.")

    if type(PersistRuntimeBoardHistory) == "function" then
      PersistRuntimeBoardHistory({
        itemKey = trim(type(entry) == "table" and entry.key or ""),
        fieldKeys = LIVE_BOARD_ASSIGNMENT_FIELD_KEYS,
      })
    end

    local distribution = type(GetBroadcastDistribution) == "function" and GetBroadcastDistribution() or nil
    if distribution and type(SendSessionComm) == "function" then
      SendSessionComm(
        "TRADE",
        distribution,
        nil,
        trim(activeSession.id),
        type(GetSessionCommProfileName) == "function" and GetSessionCommProfileName(activeSession, profileName) or trim(profileName),
        trim(entry.key),
        "TRADED",
        tostring(changedAt),
        changedBy
      )
    end

    if type(RefreshBoardAndEditor) == "function" then
      RefreshBoardAndEditor({ reason = "trade", itemKey = entry.key })
    elseif boardFrame and boardFrame.RefreshBoard then
      boardFrame:RefreshBoard()
    end
    return true
  end

  local function ExtractItemID(value)
    local text = trim(value)
    local itemID = text:match("item:(%d+):")
    if itemID then
      return tonumber(itemID)
    end
    return tonumber(text)
  end

  local function GetBagNumSlots(bag)
    if C_Container and type(C_Container.GetContainerNumSlots) == "function" then
      return tonumber(C_Container.GetContainerNumSlots(bag)) or 0
    end
    if type(GetContainerNumSlots) == "function" then
      return tonumber(GetContainerNumSlots(bag)) or 0
    end
    return 0
  end

  local function GetBagItemLink(bag, slot)
    if C_Container and type(C_Container.GetContainerItemLink) == "function" then
      return C_Container.GetContainerItemLink(bag, slot)
    end
    if type(GetContainerItemLink) == "function" then
      return GetContainerItemLink(bag, slot)
    end
    return nil
  end

  local function PickupBagItem(bag, slot)
    if C_Container and type(C_Container.PickupContainerItem) == "function" then
      C_Container.PickupContainerItem(bag, slot)
      return true
    end
    if type(PickupContainerItem) == "function" then
      PickupContainerItem(bag, slot)
      return true
    end
    return false
  end

  local function FindAssignedBagItem(entry)
    if type(entry) ~= "table" then
      return nil
    end
    local wantedLink = trim(entry.link)
    local wantedItemID = ExtractItemID(entry.itemID or entry.link)
    for bag = 0, 4 do
      local slots = GetBagNumSlots(bag)
      for slot = 1, slots do
        local link = GetBagItemLink(bag, slot)
        if link then
          if wantedLink ~= "" and trim(link) == wantedLink then
            return bag, slot, link
          end
          if wantedItemID and ExtractItemID(link) == wantedItemID then
            return bag, slot, link
          end
        end
      end
    end
    return nil
  end

  local function FindTradeSlot()
    if TradeFrame and TradeFrame.IsShown and not TradeFrame:IsShown() then
      return nil
    end
    if type(GetTradePlayerItemLink) ~= "function" then
      return 1
    end
    for slot = 1, 6 do
      if not GetTradePlayerItemLink(slot) then
        return slot
      end
    end
    return nil
  end

  local function TryPlaceTradeItem(entry)
    local bag, slot = FindAssignedBagItem(entry)
    if not bag or not slot then
      return false, "Item not found in bags."
    end
    local tradeSlot = FindTradeSlot()
    if not tradeSlot or type(ClickTradeButton) ~= "function" then
      return false, "No free trade slot is available."
    end
    local ok = pcall(function()
      if not PickupBagItem(bag, slot) then
        error("PickupContainerItem unavailable")
      end
      ClickTradeButton(tradeSlot)
    end)
    if not ok then
      return false, "WoW blocked placing the item automatically."
    end
    return true, "Item placed into trade slot " .. tostring(tradeSlot) .. ". Confirm the trade manually."
  end

  local pendingTradeBoardFrame
  local pendingTradeEntry
  local tradeEventFrame
  local RefreshTradeHelperPopup

  local function EnsureTradeEventFrame()
    if tradeEventFrame then
      return tradeEventFrame
    end
    tradeEventFrame = CreateFrame("Frame")
    tradeEventFrame:RegisterEvent("TRADE_SHOW")
    tradeEventFrame:SetScript("OnEvent", function()
      local entry = pendingTradeEntry
      local boardFrame = pendingTradeBoardFrame
      pendingTradeEntry = nil
      pendingTradeBoardFrame = nil
      if type(entry) ~= "table" then
        return
      end
      local placed, detail = TryPlaceTradeItem(entry)
      if placed then
        SetTradeState(entry.key, "PLACED", detail)
      else
        SetTradeState(entry.key, "STARTED", detail .. " Place it manually if WoW blocks automation.")
      end
      RefreshTradeHelperPopup(boardFrame)
    end)
    return tradeEventFrame
  end

  local function AttemptTrade(boardFrame, entry)
    if type(entry) ~= "table" or trim(entry.awardedTo) == "" then
      return false, "This item has no assigned player."
    end
    local target = trim(entry.awardedTo)
    local unit = type(ResolveSessionUnit) == "function" and ResolveSessionUnit(target) or nil
    if not unit then
      return false, "Assigned player is not in your group or not visible."
    end
    if unit == "player" then
      return false, "Cannot trade yourself."
    end
    if type(CheckInteractDistance) == "function" and not CheckInteractDistance(unit, 2) then
      return false, "Assigned player is not in trade range."
    end
    EnsureTradeEventFrame()
    if TradeFrame and TradeFrame.IsShown and TradeFrame:IsShown() then
      local placed, detail = TryPlaceTradeItem(entry)
      if placed then
        SetTradeState(entry.key, "PLACED", detail)
      else
        SetTradeState(entry.key, "STARTED", detail)
      end
      return placed, detail
    end
    pendingTradeBoardFrame = boardFrame
    pendingTradeEntry = entry
    if type(InitiateTrade) == "function" then
      pcall(InitiateTrade, unit)
    end
    local message = "Trade requested. The item will be placed when the trade window opens if WoW allows it."
    SetTradeState(entry.key, "STARTED", message)
    return true, message
  end

  local function CollectAssignedTradeRows()
    local rows = {}
    for _, itemKey in ipairs(RuntimeState.itemOrder or {}) do
      local entry = RuntimeState.items and RuntimeState.items[itemKey] or nil
      if type(entry) == "table" and entry.awardedAt and trim(entry.awardedTo) ~= "" then
        rows[#rows + 1] = entry
      end
    end
    return rows
  end

  local function EnsureTradeRows(frame, count)
    frame.tradeRows = frame.tradeRows or {}
    while #frame.tradeRows < count do
      local row = CreateFrame("Frame", nil, frame, "BackdropTemplate")
      row:SetSize(560, 44)
      if type(ApplyFrameSkin) == "function" then
        ApplyFrameSkin(row)
      end
      local item = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
      item:SetPoint("TOPLEFT", 10, -8)
      item:SetWidth(235)
      item:SetJustifyH("LEFT")
      row.item = item
      local target = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
      target:SetPoint("TOPLEFT", 252, -8)
      target:SetWidth(135)
      target:SetJustifyH("LEFT")
      row.target = target
      local status = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
      status:SetPoint("TOPLEFT", 10, -26)
      status:SetWidth(365)
      status:SetJustifyH("LEFT")
      row.status = status
      local tradeBtn = CreateFrame("Button", nil, row, "UIPanelButtonTemplate")
      tradeBtn:SetSize(70, 22)
      tradeBtn:SetPoint("TOPRIGHT", -108, -11)
      tradeBtn:SetText("Trade")
      if type(ApplyBoardButtonStyle) == "function" then
        ApplyBoardButtonStyle(tradeBtn)
      end
      row.tradeBtn = tradeBtn
      local markBtn = CreateFrame("Button", nil, row, "UIPanelButtonTemplate")
      markBtn:SetSize(94, 22)
      markBtn:SetPoint("TOPRIGHT", -8, -11)
      markBtn:SetText("Mark Traded")
      if type(ApplyBoardButtonStyle) == "function" then
        ApplyBoardButtonStyle(markBtn)
      end
      row.markBtn = markBtn
      frame.tradeRows[#frame.tradeRows + 1] = row
    end
    return frame.tradeRows
  end

  RefreshTradeHelperPopup = function(boardFrame)
    local popup = boardFrame and boardFrame.__tradeHelperPopup or nil
    if not popup then
      return
    end
    local rows = CollectAssignedTradeRows()
    EnsureTradeRows(popup, math.max(1, #rows))
    if popup.emptyText then
      popup.emptyText:SetShown(#rows == 0)
    end
    for index, rowFrame in ipairs(popup.tradeRows or {}) do
      local entry = rows[index]
      if entry then
        rowFrame:ClearAllPoints()
        rowFrame:SetPoint("TOPLEFT", 16, -74 - ((index - 1) * 50))
        rowFrame.item:SetText(trim(entry.name or entry.link or entry.key))
        rowFrame.target:SetText(trim(entry.awardedTo))
        local state = GetTradeState(entry.key)
        if not state and string.upper(trim(entry.tradeState)) == "TRADED" and tonumber(entry.tradedAt) then
          state = {
            state = "TRADED",
            detail = "Marked as traded by " .. tostring(trim(entry.tradedBy) ~= "" and trim(entry.tradedBy) or "the session host") .. ".",
            updatedAt = tonumber(entry.tradedAt) or 0,
          }
        end
        if type(state) == "table" and tonumber(state.updatedAt) and tonumber(entry.awardUpdatedAt or entry.awardedAt) and tonumber(state.updatedAt) < tonumber(entry.awardUpdatedAt or entry.awardedAt) then
          state = nil
        end
        local stateText = type(state) == "table" and trim(state.state) or "PENDING"
        local detail = type(state) == "table" and trim(state.detail) or "Not traded yet."
        rowFrame.status:SetText(stateText .. ": " .. detail)
        if stateText == "TRADED" then
          rowFrame.status:SetTextColor(0.35, 0.95, 0.45, 1)
        elseif stateText == "PLACED" then
          rowFrame.status:SetTextColor(1.00, 0.82, 0.28, 1)
        else
          rowFrame.status:SetTextColor(0.82, 0.82, 0.86, 1)
        end
        rowFrame.tradeBtn:SetScript("OnClick", function()
          local _, message = AttemptTrade(boardFrame, entry)
          if trim(message) ~= "" then
            local current = GetTradeState(entry.key)
            if not current then
              SetTradeState(entry.key, "STARTED", message)
            end
          end
          RefreshTradeHelperPopup(boardFrame)
        end)
        rowFrame.markBtn:SetScript("OnClick", function()
          MarkEntryTradedAndBroadcast(boardFrame, entry)
          RefreshTradeHelperPopup(boardFrame)
        end)
        rowFrame:Show()
      else
        rowFrame:Hide()
      end
    end
  end

  local function OpenTradeHelperPopup(boardFrame)
    local activeSession, profileName = GetActiveSessionContext()
    if type(activeSession) ~= "table" or type(IsSelfHost) ~= "function" or not IsSelfHost(activeSession) then
      return false
    end
    if type(CanSelfAssignLiveBoard) == "function" and not CanSelfAssignLiveBoard(activeSession, profileName) then
      return false
    end
    local popup = EnsureActionPopupFrame(boardFrame, "__tradeHelperPopup", "LIDCOUNCIL_TradeHelperPopup", 610, 520, "Trade Helper")
    if not popup.header then
      local header = popup:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
      header:SetPoint("TOPLEFT", 16, -48)
      header:SetWidth(560)
      header:SetJustifyH("LEFT")
      header:SetText("Assigned Live Board items. Trade confirmation remains manual.")
      popup.header = header
      local emptyText = popup:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
      emptyText:SetPoint("CENTER", popup, "CENTER", 0, 0)
      emptyText:SetWidth(440)
      emptyText:SetJustifyH("CENTER")
      emptyText:SetText("No assigned items are available yet.")
      popup.emptyText = emptyText
    end
    RefreshTradeHelperPopup(boardFrame)
    popup:Show()
    if popup.Raise then
      popup:Raise()
    end
    return true
  end

  return {
    OpenChangeRollPopup = OpenChangeRollPopup,
    OpenTradeHelperPopup = OpenTradeHelperPopup,
    OptionUsesDice = OptionUsesDice,
  }
end
