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

  local Print = deps.Print
  local DeepCopy = deps.DeepCopy
  local GetLootLedgerCharacters = deps.GetLootLedgerCharacters
  local AddLootLedgerEntry = deps.AddLootLedgerEntry
  local ResolveLootAwardOutcome = deps.ResolveLootAwardOutcome
  local RemoveLootLedgerEntry = deps.RemoveLootLedgerEntry
  local TrimText = deps.TrimText
  local NormalizeSelectionNote = deps.NormalizeSelectionNote
  local ResolveOptionIconTexture = deps.ResolveOptionIconTexture
  local PersistRuntimeBoardHistory = deps.PersistRuntimeBoardHistory
  local GetActiveLootSession = deps.GetActiveLootSession
  local EnsureRuntimeSession = deps.EnsureRuntimeSession
  local EnsureLiveEntry = deps.EnsureLiveEntry
  local NormalizeCharacterKey = deps.NormalizeCharacterKey
  local ResolveRollOptions = deps.ResolveRollOptions
  local GetSelectionRollValue = deps.GetSelectionRollValue
  local IsRealSimulationItemKey = deps.IsRealSimulationItemKey
  local IsSelfHost = deps.IsSelfHost
  local RecordLootRollStat = deps.RecordLootRollStat
  local RefreshEditor = deps.RefreshEditor
  local runtime = deps.runtime
  local uiState = deps.uiState
  local LIVE_BOARD_ASSIGNMENT_FIELD_KEYS = {
    "awardedTo",
    "awardedPoints",
    "awardedNote",
    "awardedAt",
    "ledgerEntryId",
    "awardUpdatedAt",
    "tradeState",
    "tradedAt",
    "tradedBy",
    "tradeUpdatedAt",
  }

  local function IsRealSimulationEntry(entry, itemKey)
    local resolvedKey = TrimText(itemKey or (type(entry) == "table" and entry.key) or "")
    if type(IsRealSimulationItemKey) == "function" then
      return IsRealSimulationItemKey(resolvedKey)
    end
    return resolvedKey ~= "" and string.sub(resolvedKey, 1, 8) == "TESTSIM-"
  end

  local function NormalizeComparableCharacterKey(value)
    return string.lower(NormalizeCharacterKey(value))
  end

  local function SplitCharacterKey(value)
    local normalized = NormalizeComparableCharacterKey(value)
    if normalized == "" then
      return "", ""
    end
    local name, realm = normalized:match("^([^%-]+)%-(.+)$")
    if name and name ~= "" then
      return name, realm or ""
    end
    return normalized, ""
  end

  local function AreCharacterKeysSame(left, right)
    local leftKey = NormalizeComparableCharacterKey(left)
    local rightKey = NormalizeComparableCharacterKey(right)
    if leftKey == "" or rightKey == "" then
      return false
    end
    if leftKey == rightKey then
      return true
    end
    local leftName, leftRealm = SplitCharacterKey(leftKey)
    local rightName, rightRealm = SplitCharacterKey(rightKey)
    return leftName ~= "" and leftName == rightName and (leftRealm == "" or rightRealm == "")
  end

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

  local function ResolveLootLedgerTotals(profileName)
    local resolvedProfileName = TrimText(profileName)
    local totals = {}
    if resolvedProfileName == "" or type(GetLootLedgerCharacters) ~= "function" then
      return totals
    end

    local rows = GetLootLedgerCharacters(resolvedProfileName) or {}
    local groupTotals = {}
    local groupRoots = {}
    for _, row in ipairs(rows) do
      local characterKey = TrimText(type(row) == "table" and row.character or "")
      if characterKey ~= "" then
        local rootKey = characterKey
        local mainCharacter = TrimText(type(row) == "table" and row.mainCharacter or "")
        local mainAltRole = string.upper(TrimText(type(row) == "table" and row.mainAltRole or "MAIN"))
        if mainAltRole == "ALT" and mainCharacter ~= "" and string.lower(mainCharacter) ~= string.lower(characterKey) then
          rootKey = mainCharacter
        else
          rootKey = characterKey
        end
        groupRoots[characterKey] = rootKey
        groupTotals[rootKey] = (tonumber(groupTotals[rootKey]) or 0) + (tonumber(row.totalPoints) or 0)
      end
    end

    for _, row in ipairs(rows) do
      local characterKey = TrimText(type(row) == "table" and row.character or "")
      if characterKey ~= "" then
        local rootKey = groupRoots[characterKey] or characterKey
        totals[characterKey] = {
          ownPoints = tonumber(row.totalPoints) or 0,
          totalPoints = tonumber(groupTotals[rootKey]) or tonumber(row.totalPoints) or 0,
          itemCount = tonumber(row.itemCount) or 0,
          bisCount = tonumber(row.bisCount) or 0,
          offSpecCount = tonumber(row.offSpecCount) or 0,
          transmogCount = tonumber(row.transmogCount) or 0,
          disenchantCount = tonumber(row.disenchantCount) or 0,
          sessionCount = tonumber(row.sessionCount) or 0,
          penaltyPoints = tonumber(row.penaltyPoints) or 0,
          mainAltRole = TrimText(row.mainAltRole),
          mainCharacter = TrimText(row.mainCharacter),
        }
      end
    end
    return totals
  end

  local function GetSelectionSortPriority(session, profileName, selection)
    local selectionKey = string.lower(TrimText(selection and (selection.optionKey or selection.key or selection.label) or "pending"))
    local selectionLabel = string.lower(TrimText(selection and (selection.label or selection.optionKey or selection.key) or ""))
    if selectionKey == "" or selectionKey == "pending" then
      return 9999
    end

    local options = ResolveRollOptions(session, profileName)
    for index, option in ipairs(options or {}) do
      local optionKey = string.lower(TrimText(type(option) == "table" and (option.key or option.label) or ""))
      local optionLabel = string.lower(TrimText(type(option) == "table" and (option.label or option.key) or ""))
      if (optionKey ~= "" and (optionKey == selectionKey or optionKey == selectionLabel))
        or (optionLabel ~= "" and (optionLabel == selectionKey or optionLabel == selectionLabel)) then
        return index
      end
    end
    return #(options or {}) + 1
  end

  local function IsFirstRollOption(session, profileName, selection)
    local options = ResolveRollOptions(session, profileName)
    local first = type(options) == "table" and options[1] or nil
    if type(first) ~= "table" or type(selection) ~= "table" then
      return false
    end
    local selectionKey = string.lower(TrimText(selection.key or selection.optionKey or selection.label))
    local selectionLabel = string.lower(TrimText(selection.label or selection.key or selection.optionKey))
    local firstKey = string.lower(TrimText(first.key or first.label))
    local firstLabel = string.lower(TrimText(first.label or first.key))
    return (selectionKey ~= "" and selectionKey == firstKey) or (selectionLabel ~= "" and selectionLabel == firstLabel)
  end

  local function GetAwardCandidateRows(session, entry, profileName)
    local rows = {}
    if type(session) ~= "table" then
      return rows
    end
    for memberKey, member in pairs(session.members or {}) do
      if tostring(member.status or "JOINED") == "JOINED" then
        local selection = ResolveEntrySelection(entry, memberKey)
        rows[#rows + 1] = {
          key = memberKey,
          name = memberKey,
          isHost = AreCharacterKeysSame(session.host or "", memberKey),
          selection = selection,
          sortPriority = selection and GetSelectionSortPriority(session, profileName, selection) or 9999,
        }
      end
    end
    table.sort(rows, function(a, b)
      local pa = GetSelectionSortPriority(session, profileName, a.selection)
      local pb = GetSelectionSortPriority(session, profileName, b.selection)
      if pa == pb then
        local ra = GetSelectionRollValue(a.selection)
        local rb = GetSelectionRollValue(b.selection)
        if ra == rb then
          return string.lower(tostring(a.name or "")) < string.lower(tostring(b.name or ""))
        end
        return ra > rb
      end
      return pa < pb
    end)
    return rows
  end

  local function EnsureSelectedMember(session, entry)
    local rows = GetAwardCandidateRows(session, entry, runtime.profileName or (type(entry) == "table" and entry.profileName) or "")
    if #rows == 0 then
      runtime.selectedMemberKey = nil
      return rows, nil
    end

    for _, row in ipairs(rows) do
      if row.key == runtime.selectedMemberKey then
        return rows, row
      end
    end

    local fallback = rows[1]
    for _, row in ipairs(rows) do
      if not row.isHost then
        fallback = row
        break
      end
    end
    runtime.selectedMemberKey = fallback and fallback.key or nil
    return rows, fallback
  end

  local function CycleSelectedMember(session, entry, delta)
    local rows, current = EnsureSelectedMember(session, entry)
    if #rows == 0 then
      runtime.selectedMemberKey = nil
      return nil
    end
    local currentIndex = 1
    for i, row in ipairs(rows) do
      if current and row.key == current.key then
        currentIndex = i
        break
      end
    end
    local offset = tonumber(delta) or 0
    local nextIndex = currentIndex + offset
    while nextIndex < 1 do
      nextIndex = nextIndex + #rows
    end
    while nextIndex > #rows do
      nextIndex = nextIndex - #rows
    end
    runtime.selectedMemberKey = rows[nextIndex].key
    return rows[nextIndex]
  end

  local function BuildAwardNote(entry, session)
    local sessionId = type(session) == "table" and tostring(session.id or "") or ""
    local itemKey = type(entry) == "table" and tostring(entry.key or "") or ""
    local parts = {}
    if sessionId ~= "" then
      parts[#parts + 1] = "Session " .. sessionId
    end
    if itemKey ~= "" then
      parts[#parts + 1] = "Item " .. itemKey
    end
    return table.concat(parts, " | ")
  end

  local function BuildAwardLedgerData(profileName, session, entry, memberKey)
    local selection = ResolveEntrySelection(entry, memberKey)
    local outcome = nil
    if type(ResolveLootAwardOutcome) == "function" then
      outcome = select(2, ResolveLootAwardOutcome(
        profileName,
        type(entry) == "table" and (entry.name or entry.link or entry.key) or "",
        type(entry) == "table" and entry.itemLevel or nil,
        type(entry) == "table" and entry.itemID or nil,
        type(entry) == "table" and entry.link or nil,
        selection,
        type(session) == "table" and session.rollOptions or nil
      ))
    end
    return {
      pointBehavior = TrimText(type(outcome) == "table" and outcome.pointBehavior or (type(selection) == "table" and selection.pointBehavior) or ""),
      counterKey = TrimText(type(outcome) == "table" and outcome.counterKey or ""),
      optionKey = TrimText(type(selection) == "table" and (selection.optionKey or selection.key) or ""),
      optionLabel = TrimText(type(selection) == "table" and selection.label or ""),
      optionColor = type(selection) == "table" and DeepCopy(selection.color) or nil,
      rollValue = tonumber(type(selection) == "table" and selection.rollValue) or nil,
      sessionId = TrimText(type(session) == "table" and session.id or ""),
    }
  end

  local function MarkAwardOnEntry(entry, memberKey, points, note, awardedAt)
    if not entry then
      return
    end
    local resolvedAwardedAt = tonumber(awardedAt) or ((type(GetServerTime) == "function" and GetServerTime()) or (time and time() or 0))
    entry.awardedTo = memberKey
    entry.awardedPoints = tonumber(points) or 0
    entry.awardedNote = note
    entry.awardedAt = resolvedAwardedAt
    entry.awardUpdatedAt = resolvedAwardedAt
  end

  local function ClearAwardOnEntry(entry, changedAt)
    if not entry then
      return
    end
    local resolvedChangedAt = tonumber(changedAt) or ((type(GetServerTime) == "function" and GetServerTime()) or (time and time() or 0))
    entry.awardedTo = nil
    entry.awardedPoints = nil
    entry.awardedNote = nil
    entry.awardedAt = nil
    entry.ledgerEntryId = nil
    entry.awardUpdatedAt = resolvedChangedAt
    entry.tradeState = nil
    entry.tradedAt = nil
    entry.tradedBy = nil
    entry.tradeUpdatedAt = resolvedChangedAt
  end

  local function RemoveAwardFromEntry(profileName, entry, changedAt)
    local resolvedProfileName = TrimText(profileName or (type(entry) == "table" and entry.profileName) or runtime.profileName)
    if type(entry) ~= "table" then
      return false, "Board entry not found."
    end
    if resolvedProfileName == "" then
      return false, "Profile not found."
    end
    local awardedTo = TrimText(entry.awardedTo)
    local ledgerEntryId = TrimText(entry.ledgerEntryId)
    if awardedTo == "" then
      return false, "This item cannot be unassigned."
    end
    if IsRealSimulationEntry(entry) then
      if tonumber(changedAt) then
        runtime.boardUpdatedAt = math.max(tonumber(runtime.boardUpdatedAt) or 0, tonumber(changedAt) or 0)
      end
      ClearAwardOnEntry(entry, changedAt)
      PersistRuntimeBoardHistory({
        itemKey = TrimText(type(entry) == "table" and entry.key or ""),
        fieldKeys = LIVE_BOARD_ASSIGNMENT_FIELD_KEYS,
      })
      return true, entry
    end
    if type(RemoveLootLedgerEntry) ~= "function" then
      return false, "Loot ledger removal is unavailable."
    end
    if ledgerEntryId == "" then
      return false, "This item cannot be unassigned."
    end

    local ok, removedEntry, ledgerRow = RemoveLootLedgerEntry(resolvedProfileName, awardedTo, ledgerEntryId)
    if not ok then
      return false, removedEntry
    end

    if tonumber(changedAt) then
      runtime.boardUpdatedAt = math.max(tonumber(runtime.boardUpdatedAt) or 0, tonumber(changedAt) or 0)
    end
    ClearAwardOnEntry(entry, changedAt)
    PersistRuntimeBoardHistory({
      itemKey = TrimText(type(entry) == "table" and entry.key or ""),
      fieldKeys = LIVE_BOARD_ASSIGNMENT_FIELD_KEYS,
    })
    return true, removedEntry, ledgerRow
  end

  local function ApplyAwardMessage(itemKey, memberKey, itemName, itemLevel, points, note, sessionOverride, profileNameOverride, changedAtOverride)
    local activeSession, activeProfileName = GetActiveLootSession()
    local profileName = TrimText(profileNameOverride or activeProfileName or runtime.profileName or "")
    local resolvedSession = type(sessionOverride) == "table" and sessionOverride or activeSession
    if profileName == "" then
      if type(Print) == "function" then
        Print("Could not write the loot award into the Loot List because no active profile was resolved.")
      end
      return false
    end

    local entry = runtime.items[itemKey]
    if entry and entry.awardedAt then
      return true
    end

    if resolvedSession and EnsureRuntimeSession(resolvedSession, profileName) then
      entry = EnsureLiveEntry(resolvedSession, profileName, itemKey, {
        name = TrimText(itemName ~= "" and itemName or (entry and entry.name) or itemKey),
        itemLevel = tonumber(itemLevel),
      })
    else
      entry = entry or {
        key = itemKey,
        name = TrimText(itemName ~= "" and itemName or itemKey),
        itemLevel = tonumber(itemLevel),
        selections = {},
      }
      runtime.items[itemKey] = entry
    end
    entry.profileName = profileName

    if entry.awardedAt then
      return true
    end

    if IsRealSimulationEntry(entry, itemKey) then
      local awardData = BuildAwardLedgerData(profileName, resolvedSession, entry, memberKey)
      if tonumber(changedAtOverride) then
        runtime.boardUpdatedAt = math.max(tonumber(runtime.boardUpdatedAt) or 0, tonumber(changedAtOverride) or 0)
      end
      MarkAwardOnEntry(entry, memberKey, points, note, changedAtOverride)
      entry.ledgerEntryId = ""
      PersistRuntimeBoardHistory({
        itemKey = TrimText(type(entry) == "table" and entry.key or ""),
        fieldKeys = LIVE_BOARD_ASSIGNMENT_FIELD_KEYS,
      })
      if type(Print) == "function" then
        local optionLabel = TrimText(type(awardData) == "table" and (awardData.optionLabel ~= "" and awardData.optionLabel or awardData.optionKey) or "")
        local optionSuffix = optionLabel ~= "" and (" for \"" .. optionLabel .. "\"") or ""
        Print(
          string.format(
            "Assigned %s to %s for %d point(s)%s. Real simulation only, no Loot List points were recorded.",
            tostring(itemName or itemKey or "?"),
            tostring(memberKey or "?"),
            tonumber(points) or 0,
            optionSuffix
          )
        )
      end
      return true, entry
    end

    local awardData = BuildAwardLedgerData(profileName, resolvedSession, entry, memberKey)
    local ok, ledgerEntry = AddLootLedgerEntry(profileName, memberKey, itemName, itemLevel, points, note, entry.itemID, entry.link, entry.icon, entry.quality, awardData)
    if not ok then
      if type(Print) == "function" then
        Print("Could not write the loot award into the Loot List: " .. tostring(ledgerEntry))
      end
      return false
    end
    if tonumber(changedAtOverride) then
      runtime.boardUpdatedAt = math.max(tonumber(runtime.boardUpdatedAt) or 0, tonumber(changedAtOverride) or 0)
    end
    MarkAwardOnEntry(entry, memberKey, points, note, changedAtOverride)
    entry.ledgerEntryId = TrimText(type(ledgerEntry) == "table" and ledgerEntry.entryId or "")
    PersistRuntimeBoardHistory({
      itemKey = TrimText(type(entry) == "table" and entry.key or ""),
      fieldKeys = LIVE_BOARD_ASSIGNMENT_FIELD_KEYS,
    })
    if type(Print) == "function" then
      local optionLabel = TrimText(type(awardData) == "table" and (awardData.optionLabel ~= "" and awardData.optionLabel or awardData.optionKey) or "")
      local optionSuffix = optionLabel ~= "" and (" for \"" .. optionLabel .. "\"") or ""
      Print(
        string.format(
          "Assigned %s to %s for %d point(s)%s in profile %s.",
          tostring(itemName or itemKey or "?"),
          tostring(memberKey or "?"),
          tonumber(points) or 0,
          optionSuffix,
          tostring(profileName)
        )
      )
    end
    return true, entry
  end

  local function RefreshBoardAndEditor(opts)
    opts = type(opts) == "table" and opts or {}
    local reason = string.lower(TrimText(opts.reason))
    local boardFrame = uiState.boardFrame
    if boardFrame and boardFrame:IsShown() and boardFrame.RefreshBoard then
      local shouldRefreshBoard = true
      if (reason == "choice" or reason == "vote") then
        local itemKey = TrimText(opts.itemKey)
        local preview = type(boardFrame.__previewState) == "table" and boardFrame.__previewState or nil
        if itemKey ~= "" and not preview and TrimText(runtime.selectedItemKey) ~= itemKey then
          shouldRefreshBoard = false
        end
      end
      if shouldRefreshBoard then
        boardFrame:RefreshBoard(opts)
      end
    end

    local refreshOpts = opts.editorRefresh
    if type(refreshOpts) ~= "table" then
      if reason == "choice" or reason == "vote" then
        refreshOpts = { refreshPages = { LiveBoard = true } }
      elseif reason == "award" or reason == "unassign" or reason == "trade" then
        refreshOpts = { refreshPages = { LiveBoard = true, Backgrounds = true } }
      elseif reason == "track" then
        refreshOpts = { refreshPages = { Panels = true, LiveBoard = true } }
      elseif reason == "sync" then
        refreshOpts = { refreshPages = { Panels = true, LiveBoard = true, Backgrounds = true } }
      else
        refreshOpts = nil
      end
    end
    RefreshEditor(refreshOpts)
  end

  local function RecordChoice(entry, memberKey, option, source)
    if not entry or TrimText(memberKey) == "" then
      return
    end
    local selectedAt = tonumber(option and option.selectedAt) or ((type(GetServerTime) == "function" and GetServerTime()) or (time and time() or 0))
    entry.selections = entry.selections or {}
    local currentSelection, storedMemberKey = ResolveEntrySelection(entry, memberKey)
    local targetMemberKey = storedMemberKey or memberKey
    local currentSelectedSeq = tonumber(type(currentSelection) == "table" and currentSelection.selectedSeq) or 0
    local selectedSeq = tonumber(option and option.selectedSeq) or 0
    if selectedSeq <= 0 then
      selectedSeq = currentSelectedSeq + 1
    else
      selectedSeq = math.max(currentSelectedSeq, math.floor(selectedSeq))
    end
    entry.selections[targetMemberKey] = {
      optionKey = TrimText(option and option.key or ""),
      label = TrimText(option and option.label or ""),
      color = DeepCopy(option and option.color or { 0.82, 0.82, 0.86 }),
      icon = ResolveOptionIconTexture(option and option.icon, option and option.key, option and option.label),
      note = NormalizeSelectionNote(option and option.note or ""),
      rollValue = tonumber(option and option.rollValue) or nil,
      pointBehavior = TrimText(option and option.pointBehavior or ""),
      selectedAt = selectedAt,
      selectedSeq = selectedSeq,
      source = source or "LOCAL",
    }
    runtime.boardUpdatedAt = math.max(tonumber(runtime.boardUpdatedAt) or 0, tonumber(selectedAt) or 0)
    PersistRuntimeBoardHistory({
      itemKey = TrimText(type(entry) == "table" and entry.key or ""),
      selectionMemberKey = targetMemberKey,
    })

    local activeSession, activeProfileName = GetActiveLootSession()
    local statOptionKey = TrimText(option and (option.key or option.optionKey) or "")
    local statOptionLabel = TrimText(option and option.label or "")
    local statOptionText = string.lower(TrimText(statOptionKey ~= "" and statOptionKey or statOptionLabel))
    if type(RecordLootRollStat) == "function"
        and type(IsSelfHost) == "function"
        and activeSession
        and IsSelfHost(activeSession)
        and not IsRealSimulationEntry(entry, entry and entry.key)
        and statOptionText ~= ""
        and statOptionText ~= "pending" then
      local recordProfileName = TrimText(activeProfileName) ~= "" and activeProfileName or runtime.profileName
      RecordLootRollStat(recordProfileName, {
        sessionId = TrimText(type(activeSession) == "table" and activeSession.id or runtime.sessionId),
        itemKey = TrimText(type(entry) == "table" and entry.key or ""),
        character = targetMemberKey,
        optionKey = statOptionKey,
        optionLabel = statOptionLabel,
        pointBehavior = TrimText(option and option.pointBehavior or ""),
        popupSection = TrimText(option and option.popupSection or ""),
        rollValue = tonumber(option and option.rollValue),
        selectedAt = selectedAt,
        isFirstOption = IsFirstRollOption(activeSession, recordProfileName, option),
      })
    end
  end

  return {
    ResolveLootLedgerTotals = ResolveLootLedgerTotals,
    GetAwardCandidateRows = GetAwardCandidateRows,
    EnsureSelectedMember = EnsureSelectedMember,
    CycleSelectedMember = CycleSelectedMember,
    BuildAwardNote = BuildAwardNote,
    MarkAwardOnEntry = MarkAwardOnEntry,
    ClearAwardOnEntry = ClearAwardOnEntry,
    RemoveAwardFromEntry = RemoveAwardFromEntry,
    ApplyAwardMessage = ApplyAwardMessage,
    RefreshBoardAndEditor = RefreshBoardAndEditor,
    RecordChoice = RecordChoice,
  }
end
