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 BroadcastAuthoritativeChoice = deps.BroadcastAuthoritativeChoice
  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 ResolveRuntimeItemDisplayText = deps.ResolveRuntimeItemDisplayText
  local TrimText = deps.TrimText
  local ResolveOptionIconTexture = deps.ResolveOptionIconTexture
  local GetSelfKey = deps.GetSelfKey
  local Print = deps.Print
  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(BroadcastAuthoritativeChoice) == "function" then
      return BroadcastAuthoritativeChoice(
        activeSession,
        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
    return false, "authoritative_choice_sender_unavailable"
  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
    local sent, sendReason = BroadcastRollOverride(activeSession, profileName, entry, memberKey, payload)
    if sent ~= true and type(Print) == "function" then
      Print("The roll override was recorded locally, but its live-board broadcast was not queued ("
        .. (trim(sendReason) ~= "" and trim(sendReason) or "transport unavailable") .. ").")
    end
    if type(RefreshBoardAndEditor) == "function" then
      RefreshBoardAndEditor({ reason = "choice", itemKey = entry.key })
    elseif boardFrame and boardFrame.RefreshBoard then
      boardFrame:RefreshBoard()
    end
    return sent == 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
      )

      local recipientKey = trim(entry.awardedTo)
      local selfKey = type(GetSelfKey) == "function" and trim(GetSelfKey()) or trim(activeSession.host)
      if recipientKey ~= "" and string.lower(recipientKey) ~= string.lower(selfKey) then
        RuntimeState.pendingTradeReceipts = type(RuntimeState.pendingTradeReceipts) == "table"
          and RuntimeState.pendingTradeReceipts
          or {}
        local receiptKey = string.lower(trim(activeSession.id))
          .. "\031" .. string.lower(trim(entry.key))
          .. "\031" .. string.lower(recipientKey)
          .. "\031" .. tostring(changedAt)
        local receipt = {
          sessionId = trim(activeSession.id),
          profileName = type(GetSessionCommProfileName) == "function"
            and GetSessionCommProfileName(activeSession, profileName)
            or trim(profileName),
          memberKey = recipientKey,
          itemKey = trim(entry.key),
          changedAt = changedAt,
          changedBy = changedBy,
          attempts = 0,
          state = "waiting",
        }
        RuntimeState.pendingTradeReceipts[receiptKey] = receipt

        local function MarkTradeReceiptUnconfirmed()
          if RuntimeState.pendingTradeReceipts[receiptKey] ~= receipt or receipt.state ~= "waiting" then
            return
          end
          receipt.state = "unconfirmed"
          RuntimeState.lastTradeReceipt = {
            sessionId = receipt.sessionId,
            itemKey = receipt.itemKey,
            memberKey = receipt.memberKey,
            changedAt = receipt.changedAt,
            state = receipt.state,
            attempts = receipt.attempts,
          }
          if type(Print) == "function" then
            Print("Trade-state delivery to " .. receipt.memberKey .. " was not confirmed. It will reconcile during board sync.")
          end
        end

        local function SendTradeReceiptRetry()
          if RuntimeState.pendingTradeReceipts[receiptKey] ~= receipt or receipt.state ~= "waiting" then
            return
          end
          local retrySession = GetActiveLootSession()
          local retryEntry = RuntimeState.items[receipt.itemKey]
          if type(retrySession) ~= "table"
            or trim(retrySession.id) ~= receipt.sessionId
            or type(retryEntry) ~= "table"
            or string.lower(trim(retryEntry.awardedTo)) ~= string.lower(receipt.memberKey)
            or string.upper(trim(retryEntry.tradeState)) ~= "TRADED"
            or tonumber(retryEntry.tradeUpdatedAt) ~= receipt.changedAt
          then
            RuntimeState.pendingTradeReceipts[receiptKey] = nil
            return
          end

          SendSessionComm(
            "TRADE",
            "WHISPER",
            receipt.memberKey,
            receipt.sessionId,
            receipt.profileName,
            receipt.itemKey,
            "TRADED",
            receipt.changedAt,
            receipt.changedBy
          )
          receipt.attempts = math.max(0, tonumber(receipt.attempts) or 0) + 1
          local retryDelays = { 1.5, 3, 6, 8 }
          local retryDelay = retryDelays[math.min(#retryDelays, receipt.attempts)]
          if type(C_Timer) == "table" and type(C_Timer.After) == "function" then
            C_Timer.After(retryDelay, function()
              if (tonumber(receipt.attempts) or 0) >= #retryDelays then
                MarkTradeReceiptUnconfirmed()
              else
                SendTradeReceiptRetry()
              end
            end)
          else
            MarkTradeReceiptUnconfirmed()
          end
        end

        if type(C_Timer) == "table" and type(C_Timer.After) == "function" then
          C_Timer.After(1.5, SendTradeReceiptRetry)
        else
          MarkTradeReceiptUnconfirmed()
        end
      end
    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 RefreshTradeHelperRangeStates

  local function HideTradeHelperForTrade(boardFrame)
    local popup = boardFrame and boardFrame.__tradeHelperPopup or nil
    if popup and popup:IsShown() then
      popup:Hide()
    end
  end

  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
      -- The native Trade window must remain unobstructed for confirmation.
      HideTradeHelperForTrade(boardFrame)
    end)
    return tradeEventFrame
  end

  local function AttemptTrade(boardFrame, entry)
    if type(InCombatLockdown) == "function" and InCombatLockdown() then
      return false, "Trade helper is unavailable during combat."
    end
    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, resolveReason
    if type(ResolveSessionUnit) == "function" then
      unit, resolveReason = ResolveSessionUnit(target)
    end
    if not unit then
      if resolveReason == "unit_api_unavailable" then
        return false, "WoW's group-unit lookup is unavailable right now."
      end
      return false, "Assigned player could not be matched to a current group unit."
    end
    if unit == "player" then
      return false, "Cannot trade yourself."
    end
    if type(UnitIsVisible) == "function" and not UnitIsVisible(unit) then
      return false, "Assigned player is in the group but is not visible."
    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)
        HideTradeHelperForTrade(boardFrame)
      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 ResolveTradeRangeState(entry)
    local target = trim(type(entry) == "table" and entry.awardedTo or "")
    if target == "" then
      return "Range unknown", 0.72, 0.72, 0.76
    end
    local unit, resolveReason
    if type(ResolveSessionUnit) == "function" then
      unit, resolveReason = ResolveSessionUnit(target)
    end
    if not unit then
      if resolveReason == "unit_api_unavailable" then
        return "Range unavailable", 0.72, 0.72, 0.76
      end
      return "Unit unresolved", 1.00, 0.72, 0.24
    end
    if unit == "player" then
      return "Your item", 0.72, 0.72, 0.76
    end
    if type(UnitIsVisible) == "function" and not UnitIsVisible(unit) then
      return "Not visible", 1.00, 0.72, 0.24
    end
    if type(CheckInteractDistance) ~= "function" then
      return "Unavailable", 0.72, 0.72, 0.76
    end
    if CheckInteractDistance(unit, 2) then
      return "In range", 0.38, 0.94, 0.50
    end
    return "Out of range", 1.00, 0.72, 0.24
  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(608, 44)
      row:EnableMouse(true)
      row.bg = row:CreateTexture(nil, "BACKGROUND")
      row.bg:SetAllPoints(true)
      row.bg:SetTexture("Interface\\Buttons\\WHITE8x8")
      row.bg:SetVertexColor(0.05, 0.06, 0.08, 0.88)
      row.hover = row:CreateTexture(nil, "ARTWORK")
      row.hover:SetAllPoints(true)
      row.hover:SetTexture("Interface\\Buttons\\WHITE8x8")
      row.hover:SetVertexColor(0.18, 0.34, 0.50, 0.18)
      row.hover:Hide()
      row.separator = row:CreateTexture(nil, "BORDER")
      row.separator:SetPoint("BOTTOMLEFT", 0, 0)
      row.separator:SetPoint("BOTTOMRIGHT", 0, 0)
      row.separator:SetHeight(1)
      row.separator:SetTexture("Interface\\Buttons\\WHITE8x8")
      row.separator:SetVertexColor(0.34, 0.38, 0.44, 0.48)
      row:SetScript("OnEnter", function(self)
        if self.hover then
          self.hover:Show()
        end
      end)
      row:SetScript("OnLeave", function(self)
        if self.hover then
          self.hover:Hide()
        end
      end)
      local itemIcon = row:CreateTexture(nil, "ARTWORK")
      itemIcon:SetPoint("TOPLEFT", 10, -6)
      itemIcon:SetSize(32, 32)
      row.itemIcon = itemIcon
      local item = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
      item:SetPoint("TOPLEFT", 50, -6)
      item:SetWidth(328)
      item:SetJustifyH("LEFT")
      row.item = item
      local target = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
      target:SetPoint("TOPLEFT", 50, -25)
      target:SetWidth(160)
      target:SetJustifyH("LEFT")
      row.target = target
      local status = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
      status:SetPoint("TOPLEFT", 216, -25)
      status:SetWidth(58)
      status:SetJustifyH("LEFT")
      row.status = status
      local rangeText = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
      rangeText:SetPoint("TOPLEFT", 280, -25)
      rangeText:SetWidth(96)
      rangeText:SetJustifyH("LEFT")
      row.rangeText = rangeText
      local tradeBtn = CreateFrame("Button", nil, row, "UIPanelButtonTemplate")
      tradeBtn:SetSize(82, 22)
      tradeBtn:SetPoint("TOPRIGHT", -108, -11)
      tradeBtn:SetText("Start Trade")
      tradeBtn.__lidBoardSelected = true
      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")
      markBtn.__lidBoardWindowControl = true
      if type(ApplyBoardButtonStyle) == "function" then
        ApplyBoardButtonStyle(markBtn)
      end
      row.markBtn = markBtn
      frame.tradeRows[#frame.tradeRows + 1] = row
    end
    return frame.tradeRows
  end

  RefreshTradeHelperRangeStates = function(popup)
    if not popup or not popup:IsShown() then
      return
    end
    for _, rowFrame in ipairs(popup.tradeRows or {}) do
      local entry = rowFrame.__tradeEntry
      if type(entry) == "table" and rowFrame.rangeText then
        if rowFrame.__tradeStateText == "TRADED" then
          rowFrame.rangeText:SetText("")
        else
          local rangeLabel, rangeR, rangeG, rangeB = ResolveTradeRangeState(entry)
          rowFrame.rangeText:SetText(rangeLabel)
          rowFrame.rangeText:SetTextColor(rangeR, rangeG, rangeB, 1)
        end
      end
    end
  end

  local function GetEffectiveTradeState(entry)
    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
    return state, type(state) == "table" and trim(state.state) or "PENDING"
  end

  RefreshTradeHelperPopup = function(boardFrame)
    local popup = boardFrame and boardFrame.__tradeHelperPopup or nil
    if not popup or not popup:IsShown() then
      return
    end
    local rows = CollectAssignedTradeRows()
    local rowsPerPage = 7
    local pageCount = math.max(1, math.ceil(#rows / rowsPerPage))
    local page = math.max(1, math.min(tonumber(popup.__tradePage) or 1, pageCount))
    local firstRow = ((page - 1) * rowsPerPage) + 1
    local visibleCount = math.max(0, math.min(rowsPerPage, #rows - firstRow + 1))
    popup.__tradePage = page
    EnsureTradeRows(popup, math.max(1, visibleCount))
    local confirmedCount = 0
    for _, entry in ipairs(rows) do
      local _, stateText = GetEffectiveTradeState(entry)
      if stateText == "TRADED" then
        confirmedCount = confirmedCount + 1
      end
    end
    if popup.emptyText then
      popup.emptyText:SetShown(#rows == 0)
    end
    if popup.hint then
      popup.hint:SetTextColor(0.72, 0.72, 0.76, 1)
      popup.hint:SetText("Trading must still be confirmed manually.")
    end
    for index, rowFrame in ipairs(popup.tradeRows or {}) do
      local entry = rows[firstRow + index - 1]
      if entry then
        rowFrame.__tradeEntry = entry
        rowFrame:ClearAllPoints()
        rowFrame:SetPoint("TOPLEFT", 16, -92 - ((index - 1) * 48))
        rowFrame.itemIcon:SetTexture(trim(entry.icon) ~= "" and entry.icon or "Interface\\Icons\\INV_Misc_QuestionMark")
        rowFrame.item:SetText(type(ResolveRuntimeItemDisplayText) == "function"
          and ResolveRuntimeItemDisplayText(entry, entry.key)
          or trim(entry.name or entry.key))
        rowFrame.target:SetText("Recipient: " .. trim(entry.awardedTo))
        local _, stateText = GetEffectiveTradeState(entry)
        rowFrame.__tradeStateText = stateText
        local statusLabel = stateText == "TRADED" and "Traded"
          or (stateText == "PLACED" and "In trade" or (stateText == "STARTED" and "Started" or "Pending"))
        rowFrame.status:SetText(statusLabel)
        if stateText == "TRADED" then
          rowFrame.status:SetTextColor(0.35, 0.95, 0.45, 1)
          rowFrame.bg:SetVertexColor(0.04, 0.12, 0.07, 0.72)
          rowFrame.separator:SetVertexColor(0.20, 0.50, 0.28, 0.42)
        elseif stateText == "PLACED" then
          rowFrame.status:SetTextColor(1.00, 0.82, 0.28, 1)
          rowFrame.bg:SetVertexColor(0.11, 0.09, 0.04, 0.88)
          rowFrame.separator:SetVertexColor(0.48, 0.36, 0.16, 0.46)
        else
          rowFrame.status:SetTextColor(0.82, 0.82, 0.86, 1)
          rowFrame.bg:SetVertexColor(0.05, 0.06, 0.08, 0.88)
          rowFrame.separator:SetVertexColor(0.34, 0.38, 0.44, 0.48)
        end
        rowFrame.__confirmItemKey = nil
        rowFrame.markBtn:SetText("Mark Traded")
        rowFrame.markBtn.__lidBoardSelected = false
        if type(ApplyBoardButtonStyle) == "function" then
          ApplyBoardButtonStyle(rowFrame.markBtn)
        end
        rowFrame.tradeBtn:SetScript("OnClick", function()
          local started, message = AttemptTrade(boardFrame, entry)
          if not started then
            if popup.hint then
              popup.hint:SetTextColor(1.00, 0.48, 0.38, 1)
              popup.hint:SetText(trim(message) ~= "" and message or "Trade could not be started.")
            end
            return
          end
          RefreshTradeHelperPopup(boardFrame)
        end)
        rowFrame.markBtn:SetScript("OnClick", function()
          if rowFrame.__confirmItemKey ~= entry.key then
            rowFrame.__confirmItemKey = entry.key
            rowFrame.markBtn:SetText("Confirm")
            rowFrame.markBtn.__lidBoardSelected = true
            if type(ApplyBoardButtonStyle) == "function" then
              ApplyBoardButtonStyle(rowFrame.markBtn)
            end
            if type(C_Timer) == "table" and type(C_Timer.After) == "function" then
              C_Timer.After(3, function()
                if rowFrame.__confirmItemKey == entry.key then
                  rowFrame.__confirmItemKey = nil
                  rowFrame.markBtn:SetText("Mark Traded")
                  rowFrame.markBtn.__lidBoardSelected = false
                  if type(ApplyBoardButtonStyle) == "function" then
                    ApplyBoardButtonStyle(rowFrame.markBtn)
                  end
                end
              end)
            end
            return
          end
          MarkEntryTradedAndBroadcast(boardFrame, entry)
          RefreshTradeHelperPopup(boardFrame)
        end)
        local isTraded = stateText == "TRADED"
        rowFrame.tradeBtn:SetShown(not isTraded)
        rowFrame.markBtn:SetShown(not isTraded)
        rowFrame:Show()
      else
        rowFrame.__tradeEntry = nil
        rowFrame.__tradeStateText = nil
        rowFrame:Hide()
      end
    end
    if popup.header then
      local pendingCount = math.max(0, #rows - confirmedCount)
      popup.header:SetText(tostring(pendingCount) .. " awaiting confirmation | " .. tostring(confirmedCount) .. " / " .. tostring(#rows) .. " traded")
    end
    if popup.pageText then
      popup.pageText:SetText("Page " .. tostring(page) .. " / " .. tostring(pageCount))
    end
    if popup.previousPageBtn then
      popup.previousPageBtn:SetEnabled(page > 1)
    end
    if popup.nextPageBtn then
      popup.nextPageBtn:SetEnabled(page < pageCount)
    end
    RefreshTradeHelperRangeStates(popup)
  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", 640, 470, "Trade Helper")
    -- Keep this helper above the Live Board; it hides as soon as trade opens.
    popup:SetFrameStrata("FULLSCREEN_DIALOG")
    if boardFrame and boardFrame.GetFrameLevel and popup.SetFrameLevel then
      popup:SetFrameLevel((boardFrame:GetFrameLevel() or 0) + 20)
    end
    if not popup.header then
      popup:HookScript("OnShow", function(self)
        self.__tradeRangeRefreshElapsed = 0
        self:SetScript("OnUpdate", function(rangePopup, elapsed)
          rangePopup.__tradeRangeRefreshElapsed = (tonumber(rangePopup.__tradeRangeRefreshElapsed) or 0) + (tonumber(elapsed) or 0)
          if rangePopup.__tradeRangeRefreshElapsed < 0.75 then
            return
          end
          rangePopup.__tradeRangeRefreshElapsed = 0
          RefreshTradeHelperRangeStates(rangePopup)
        end)
      end)
      popup:HookScript("OnHide", function(self)
        self:SetScript("OnUpdate", nil)
        self.__tradeRangeRefreshElapsed = 0
      end)
      if popup.title then
        popup.title:SetTextColor(0.95, 0.86, 0.58, 1)
      end
      if popup.closeBtn then
        popup.closeBtn:SetSize(24, 22)
        popup.closeBtn:SetText("X")
        popup.closeBtn.__lidBoardWindowControl = true
        if type(ApplyBoardButtonStyle) == "function" then
          ApplyBoardButtonStyle(popup.closeBtn)
        end
        popup.closeBtn:SetScript("OnEnter", function(self)
          if type(GameTooltip) == "table" then
            GameTooltip:SetOwner(self, "ANCHOR_BOTTOM")
            GameTooltip:SetText("Close Trade Helper", 0.92, 0.92, 0.96, 1)
            GameTooltip:Show()
          end
        end)
        popup.closeBtn:SetScript("OnLeave", function()
          if type(GameTooltip) == "table" then
            GameTooltip:Hide()
          end
        end)
      end
      local header = popup:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
      header:SetPoint("TOPLEFT", 16, -48)
      header:SetWidth(600)
      header:SetJustifyH("LEFT")
      header:SetText("")
      popup.header = header
      local hint = popup:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
      hint:SetPoint("TOPLEFT", 16, -67)
      hint:SetWidth(600)
      hint:SetJustifyH("LEFT")
      hint:SetTextColor(0.72, 0.72, 0.76, 1)
      hint:SetText("Trading must still be confirmed manually.")
      popup.hint = hint
      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
      local previousPageBtn = CreateFrame("Button", nil, popup, "UIPanelButtonTemplate")
      previousPageBtn:SetSize(86, 22)
      previousPageBtn:SetPoint("BOTTOMLEFT", 16, 16)
      previousPageBtn:SetText("Previous")
      previousPageBtn.__lidBoardWindowControl = true
      if type(ApplyBoardButtonStyle) == "function" then
        ApplyBoardButtonStyle(previousPageBtn)
      end
      previousPageBtn:SetScript("OnClick", function()
        popup.__tradePage = math.max(1, (tonumber(popup.__tradePage) or 1) - 1)
        RefreshTradeHelperPopup(boardFrame)
      end)
      popup.previousPageBtn = previousPageBtn
      local pageText = popup:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
      pageText:SetPoint("BOTTOM", popup, "BOTTOM", 0, 21)
      pageText:SetWidth(160)
      pageText:SetJustifyH("CENTER")
      pageText:SetText("Page 1 / 1")
      popup.pageText = pageText
      local nextPageBtn = CreateFrame("Button", nil, popup, "UIPanelButtonTemplate")
      nextPageBtn:SetSize(86, 22)
      nextPageBtn:SetPoint("BOTTOMRIGHT", -16, 16)
      nextPageBtn:SetText("Next")
      nextPageBtn.__lidBoardWindowControl = true
      if type(ApplyBoardButtonStyle) == "function" then
        ApplyBoardButtonStyle(nextPageBtn)
      end
      nextPageBtn:SetScript("OnClick", function()
        popup.__tradePage = (tonumber(popup.__tradePage) or 1) + 1
        RefreshTradeHelperPopup(boardFrame)
      end)
      popup.nextPageBtn = nextPageBtn
    end
    popup:Show()
    RefreshTradeHelperPopup(boardFrame)
    if popup.Raise then
      popup:Raise()
    end
    return true
  end

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