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

local shared = INTERNAL_API.LIDCOUNCIL_LootEditorShared_Impl
if type(shared) ~= "table" then
  error("LidCouncil missing loot editor shared helpers.")
end

local TrimText = shared.TrimText
local AddLootLedgerEntry = shared.AddLootLedgerEntry
local GetLootLedgerCharacters = shared.GetLootLedgerCharacters
local GetActiveLootSession = shared.GetActiveLootSession
local TouchLootProfileDomain = shared.ResolveOptionalApiFunction and shared.ResolveOptionalApiFunction("LIDCOUNCIL_TouchLootProfileDomain") or nil
local GetLootProfileDomainRevision = shared.ResolveOptionalApiFunction and shared.ResolveOptionalApiFunction("LIDCOUNCIL_GetLootProfileDomainRevision") or nil
local GetEnglishClassAndSpec = shared.ResolveOptionalApiFunction and shared.ResolveOptionalApiFunction("LIDCOUNCIL_GetEnglishClassAndSpec") or nil

local MAX_LOOTLIST_GROUP_NAME = 32
local LOOT_LEDGER_TEST_ROSTER_VERSION = 2
local ROSTER_INFO_CACHE_TTL_SECONDS = 1
local rosterCharacterInfoCache = {}
local LOOT_LEDGER_TEST_ROSTER = {
  {
    character = "Tharion-Illidan",
    className = "Warrior",
    classFile = "WARRIOR",
    specName = "Fury",
    specIcon = "Interface\\Icons\\Ability_Warrior_InnerRage",
    note = "Main melee",
    mainAltRole = "MAIN",
    demoItem = { name = "Demo Greathelm", itemLevel = 278, points = 6 },
  },
  {
    character = "Elaris-Area52",
    className = "Priest",
    classFile = "PRIEST",
    specName = "Holy",
    specIcon = "Interface\\Icons\\Spell_Holy_GuardianSpirit",
    note = "Healing lead",
    mainAltRole = "MAIN",
    demoItem = { name = "Demo Censer", itemLevel = 276, points = 5 },
  },
  {
    character = "Myrddin-Stormrage",
    className = "Mage",
    classFile = "MAGE",
    specName = "Frost",
    specIcon = "Interface\\Icons\\Spell_Frost_FrostBolt02",
    note = "Ranged main",
    mainAltRole = "MAIN",
    demoItem = { name = "Demo Icewand", itemLevel = 279, points = 7 },
  },
  {
    character = "Kaevor-Sargeras",
    className = "Druid",
    classFile = "DRUID",
    specName = "Guardian",
    specIcon = "Interface\\Icons\\Ability_Racial_BearForm",
    note = "Tank main",
    mainAltRole = "MAIN",
    demoItem = { name = "Demo Barkguard", itemLevel = 275, points = 4 },
  },
  {
    character = "Rynessa-Tichondrius",
    className = "Hunter",
    classFile = "HUNTER",
    specName = "Beast Mastery",
    specIcon = "Interface\\Icons\\Ability_Hunter_BeastTaming",
    note = "Tharion alt",
    mainAltRole = "ALT",
    mainCharacter = "Tharion-Illidan",
    demoItem = { name = "Demo Tracker Bow", itemLevel = 272, points = 3 },
  },
  {
    character = "Vorlan-Hyjal",
    className = "Paladin",
    classFile = "PALADIN",
    specName = "Retribution",
    specIcon = "Interface\\Icons\\Spell_Holy_AuraOfLight",
    note = "Weekend alt",
    mainAltRole = "ALT",
    mainCharacter = "Tharion-Illidan",
    demoItem = { name = "Demo Vindicator Blade", itemLevel = 274, points = 4 },
  },
  {
    character = "Serapha-Proudmoore",
    className = "Shaman",
    classFile = "SHAMAN",
    specName = "Restoration",
    specIcon = "Interface\\Icons\\Spell_Nature_MagicImmunity",
    note = "Raid healer alt",
    mainAltRole = "ALT",
    mainCharacter = "Elaris-Area52",
    demoItem = { name = "Demo Tidecaller Totem", itemLevel = 271, points = 2 },
  },
  {
    character = "Nythera-Dalaran",
    className = "Warlock",
    classFile = "WARLOCK",
    specName = "Affliction",
    specIcon = "Interface\\Icons\\Spell_Shadow_DeathCoil",
    note = "Caster alt",
    mainAltRole = "ALT",
    mainCharacter = "Elaris-Area52",
    demoItem = { name = "Demo Soul Codex", itemLevel = 273, points = 3 },
  },
  {
    character = "Kharos-MalGanis",
    className = "Demon Hunter",
    classFile = "DEMONHUNTER",
    specName = "Havoc",
    specIcon = "Interface\\Icons\\Ability_DemonHunter_SpecDPS",
    note = "Farm alt",
    mainAltRole = "ALT",
    mainCharacter = "Myrddin-Stormrage",
    demoItem = { name = "Demo Warglaive", itemLevel = 277, points = 5 },
  },
  {
    character = "Talvyr-Stormreaver",
    className = "Monk",
    classFile = "MONK",
    specName = "Windwalker",
    specIcon = "Interface\\Icons\\Spell_Monk_WindWalker_Spec",
    note = "Fast alt",
    mainAltRole = "ALT",
    mainCharacter = "Kaevor-Sargeras",
    demoItem = { name = "Demo Jade Knuckles", itemLevel = 270, points = 2 },
  },
  {
    character = "Aeloria-Silvermoon",
    className = "Evoker",
    classFile = "EVOKER",
    specName = "Augmentation",
    specIcon = "Interface\\Icons\\ClassIcon_Evoker",
    note = "Support main",
    mainAltRole = "MAIN",
    demoItem = { name = "Demo Scalefocus", itemLevel = 281, points = 8 },
  },
  {
    character = "Dornak-BleedingHollow",
    className = "Death Knight",
    classFile = "DEATHKNIGHT",
    specName = "Blood",
    specIcon = "Interface\\Icons\\Spell_Deathknight_BloodPresence",
    note = "Off tank main",
    mainAltRole = "MAIN",
    demoItem = { name = "Demo Bonewall", itemLevel = 280, points = 7 },
  },
  {
    character = "Velcira-Zuljin",
    className = "Rogue",
    classFile = "ROGUE",
    specName = "Assassination",
    specIcon = "Interface\\Icons\\Ability_Rogue_DeadlyBrew",
    note = "Dagger main",
    mainAltRole = "MAIN",
    demoItem = { name = "Demo Venomshiv", itemLevel = 282, points = 8 },
  },
  {
    character = "Solenne-MoonGuard",
    className = "Priest",
    classFile = "PRIEST",
    specName = "Discipline",
    specIcon = "Interface\\Icons\\Spell_Holy_PowerWordShield",
    note = "Absorb healer",
    mainAltRole = "MAIN",
    demoItem = { name = "Demo Aegis Rod", itemLevel = 279, points = 7 },
  },
  {
    character = "Brakkor-Blackhand",
    className = "Warrior",
    classFile = "WARRIOR",
    specName = "Arms",
    specIcon = "Interface\\Icons\\Ability_Warrior_SavageBlow",
    note = "Bench main",
    mainAltRole = "MAIN",
    demoItem = { name = "Demo Cleaver", itemLevel = 278, points = 6 },
  },
  {
    character = "Lysanthir-SilverHand",
    className = "Hunter",
    classFile = "HUNTER",
    specName = "Survival",
    specIcon = "Interface\\Icons\\Ability_Hunter_Camouflage",
    note = "Trap main",
    mainAltRole = "MAIN",
    demoItem = { name = "Demo Glaivebow", itemLevel = 277, points = 5 },
  },
  {
    character = "Fenrik-Illidan",
    className = "Shaman",
    classFile = "SHAMAN",
    specName = "Enhancement",
    specIcon = "Interface\\Icons\\Spell_Shaman_ImprovedStormstrike",
    note = "Tharion burst alt",
    mainAltRole = "ALT",
    mainCharacter = "Tharion-Illidan",
    demoItem = { name = "Demo Stormaxe", itemLevel = 274, points = 4 },
  },
  {
    character = "Cyrandor-Sargeras",
    className = "Mage",
    classFile = "MAGE",
    specName = "Arcane",
    specIcon = "Interface\\Icons\\Spell_Holy_MagicalSentry",
    note = "Myrddin alt",
    mainAltRole = "ALT",
    mainCharacter = "Myrddin-Stormrage",
    demoItem = { name = "Demo Prism", itemLevel = 276, points = 5 },
  },
  {
    character = "Liora-Proudmoore",
    className = "Priest",
    classFile = "PRIEST",
    specName = "Shadow",
    specIcon = "Interface\\Icons\\Spell_Shadow_ShadowWordPain",
    note = "Solenne alt",
    mainAltRole = "ALT",
    mainCharacter = "Solenne-MoonGuard",
    demoItem = { name = "Demo Voidbrand", itemLevel = 275, points = 4 },
  },
  {
    character = "Vexis-Tichondrius",
    className = "Rogue",
    classFile = "ROGUE",
    specName = "Outlaw",
    specIcon = "Interface\\Icons\\Ability_Rogue_Waylay",
    note = "Velcira alt",
    mainAltRole = "ALT",
    mainCharacter = "Velcira-Zuljin",
    demoItem = { name = "Demo Cutlass", itemLevel = 273, points = 3 },
  },
  {
    character = "Orenth-Area52",
    className = "Evoker",
    classFile = "EVOKER",
    specName = "Devastation",
    specIcon = "Interface\\Icons\\ClassIcon_Evoker",
    note = "Aeloria alt",
    mainAltRole = "ALT",
    mainCharacter = "Aeloria-Silvermoon",
    demoItem = { name = "Demo Embercore", itemLevel = 278, points = 6 },
  },
  {
    character = "Morgrym-MalGanis",
    className = "Death Knight",
    classFile = "DEATHKNIGHT",
    specName = "Frost",
    specIcon = "Interface\\Icons\\Spell_Deathknight_FrostPresence",
    note = "Dornak alt",
    mainAltRole = "ALT",
    mainCharacter = "Dornak-BleedingHollow",
    demoItem = { name = "Demo Chillblade", itemLevel = 276, points = 5 },
  },
  {
    character = "Calethas-Stormrage",
    className = "Hunter",
    classFile = "HUNTER",
    specName = "Marksmanship",
    specIcon = "Interface\\Icons\\Ability_Hunter_FocusedAim",
    note = "Lysanthir alt",
    mainAltRole = "ALT",
    mainCharacter = "Lysanthir-SilverHand",
    demoItem = { name = "Demo Eagle Eye", itemLevel = 272, points = 3 },
  },
  {
    character = "Ysella-AeriePeak",
    className = "Paladin",
    classFile = "PALADIN",
    specName = "Holy",
    specIcon = "Interface\\Icons\\Spell_Holy_HolyBolt",
    note = "Solenne healer alt",
    mainAltRole = "ALT",
    mainCharacter = "Solenne-MoonGuard",
    demoItem = { name = "Demo Sunsigil", itemLevel = 274, points = 4 },
  },
  {
    character = "Torven-Cenarius",
    className = "Monk",
    classFile = "MONK",
    specName = "Brewmaster",
    specIcon = "Interface\\Icons\\Spell_Monk_Brewmaster_Spec",
    note = "Kaevor tank alt",
    mainAltRole = "ALT",
    mainCharacter = "Kaevor-Sargeras",
    demoItem = { name = "Demo Ironskin Keg", itemLevel = 271, points = 2 },
  },
}

local function TouchLootListProfile(profile)
  if type(profile) ~= "table" then
    return
  end
  if type(TouchLootProfileDomain) == "function" then
    TouchLootProfileDomain(profile, "operational")
    return
  end
  profile.meta = profile.meta or {}
  profile.meta.revision = math.max(0, tonumber(profile.meta.revision) or 0) + 1
  profile.meta.updatedAt = time and time() or 0
end

local function EnsureLootLedgerMetaLocal(profile)
  profile = type(profile) == "table" and profile or {}
  profile.lootLedgerMeta = profile.lootLedgerMeta or {}
  for key, meta in pairs(profile.lootLedgerMeta) do
    local resolvedKey = TrimText(key)
    local entry = type(meta) == "table" and meta or {}
    entry.penaltyPoints = tonumber(entry.penaltyPoints) or 0
    entry.classFile = TrimText(entry.classFile)
    entry.className = TrimText(entry.className)
    entry.specName = TrimText(entry.specName)
    entry.specIcon = TrimText(entry.specIcon)
    entry.note = TrimText(entry.note)
    entry.mainCharacter = TrimText(entry.mainCharacter)
    entry.mainAltRole = string.upper(TrimText(entry.mainAltRole))
    if entry.mainAltRole ~= "ALT" then
      entry.mainAltRole = "MAIN"
    end
    if entry.mainAltRole == "MAIN" and entry.mainCharacter ~= "" then
      entry.mainAltRole = "ALT"
    end
    if string.lower(entry.mainCharacter) == string.lower(resolvedKey) then
      entry.mainCharacter = ""
      entry.mainAltRole = "MAIN"
    end
    entry.summaryWith = nil
    if resolvedKey ~= key then
      profile.lootLedgerMeta[key] = nil
      if resolvedKey ~= "" then
        profile.lootLedgerMeta[resolvedKey] = entry
      end
    else
      profile.lootLedgerMeta[key] = entry
    end
  end
  return profile.lootLedgerMeta
end

local function EnsureLootLedgerViewLocal(profile)
  profile = type(profile) == "table" and profile or {}
  profile.lootLedgerView = profile.lootLedgerView or {}
  profile.lootLedgerView.collapsedGroups = profile.lootLedgerView.collapsedGroups or {}
  local normalized = {}
  for key, value in pairs(profile.lootLedgerView.collapsedGroups) do
    local resolvedKey = TrimText(key)
    if resolvedKey ~= "" then
      normalized[resolvedKey] = value and true or false
    end
  end
  profile.lootLedgerView.collapsedGroups = normalized
  return profile.lootLedgerView
end

local function IsLootLedgerGroupCollapsedLocal(profile, characterKey, memberCount)
  local resolvedKey = TrimText(characterKey)
  if resolvedKey == "" then
    return false
  end
  if tonumber(memberCount) and tonumber(memberCount) <= 1 then
    return false
  end
  local view = EnsureLootLedgerViewLocal(profile)
  if view.collapsedGroups[resolvedKey] == nil then
    view.collapsedGroups[resolvedKey] = true
  end
  return view.collapsedGroups[resolvedKey] == true
end

local function ToggleLootLedgerGroupCollapsedLocal(profile, characterKey)
  local resolvedKey = TrimText(characterKey)
  if resolvedKey == "" then
    return false
  end
  local view = EnsureLootLedgerViewLocal(profile)
  view.collapsedGroups[resolvedKey] = not (view.collapsedGroups[resolvedKey] == true)
  TouchLootListProfile(profile)
  return true, view.collapsedGroups[resolvedKey] == true
end

local function NormalizeLootLedgerRoleLocal(role)
  local text = string.upper(TrimText(role))
  if text == "ALT" then
    return "ALT"
  end
  return "MAIN"
end

local function ResolveLootLedgerMainTarget(metaByCharacter, byCharacter, characterKey, mainCharacter)
  local resolvedCharacter = TrimText(characterKey)
  local resolvedMain = TrimText(mainCharacter)
  if resolvedCharacter == "" or resolvedMain == "" or string.lower(resolvedCharacter) == string.lower(resolvedMain) then
    return nil
  end

  local lowerToCharacter = {}
  for candidateKey in pairs(byCharacter or {}) do
    lowerToCharacter[string.lower(tostring(candidateKey))] = candidateKey
  end

  local matchedCharacter = lowerToCharacter[string.lower(resolvedMain)]
  if not matchedCharacter then
    return nil
  end

  local targetMeta = type(metaByCharacter[matchedCharacter]) == "table" and metaByCharacter[matchedCharacter] or nil
  if NormalizeLootLedgerRoleLocal(targetMeta and targetMeta.mainAltRole) ~= "MAIN" then
    return nil
  end
  return matchedCharacter
end

local RemoveCharacterFromLootListGroups

local function SetLootLedgerMainAltRole(profile, characterKey, role)
  local resolvedCharacter = TrimText(characterKey)
  if resolvedCharacter == "" then
    return false, "Select a character first."
  end

  local metaByCharacter = EnsureLootLedgerMetaLocal(profile)
  local meta = type(metaByCharacter[resolvedCharacter]) == "table" and metaByCharacter[resolvedCharacter] or { penaltyPoints = 0 }
  local nextRole = NormalizeLootLedgerRoleLocal(role)

  if nextRole == "ALT" then
    for otherCharacter, otherMeta in pairs(metaByCharacter) do
      if otherCharacter ~= resolvedCharacter and type(otherMeta) == "table" and string.lower(TrimText(otherMeta.mainCharacter)) == string.lower(resolvedCharacter) then
        return false, "Detach or reassign this character's alts before turning it into an Alt."
      end
    end
    meta.mainAltRole = "ALT"
    meta.mainCharacter = TrimText(meta.mainCharacter)
  else
    meta.mainAltRole = "MAIN"
    meta.mainCharacter = ""
  end

  metaByCharacter[resolvedCharacter] = meta
  TouchLootListProfile(profile)
  return true
end

local function SetLootLedgerCharacterNote(profile, characterKey, noteText)
  local resolvedCharacter = TrimText(characterKey)
  if resolvedCharacter == "" then
    return false, "Select a character first."
  end

  local metaByCharacter = EnsureLootLedgerMetaLocal(profile)
  local meta = type(metaByCharacter[resolvedCharacter]) == "table" and metaByCharacter[resolvedCharacter] or { penaltyPoints = 0 }
  meta.note = TrimText(noteText)
  metaByCharacter[resolvedCharacter] = meta
  TouchLootListProfile(profile)
  return true, meta.note
end

local function SetLootLedgerMainCharacter(profile, characterKey, mainCharacter, byCharacter)
  local resolvedCharacter = TrimText(characterKey)
  if resolvedCharacter == "" then
    return false, "Select a character first."
  end

  local metaByCharacter = EnsureLootLedgerMetaLocal(profile)
  local meta = type(metaByCharacter[resolvedCharacter]) == "table" and metaByCharacter[resolvedCharacter] or { penaltyPoints = 0 }
  local resolvedMain = ResolveLootLedgerMainTarget(metaByCharacter, byCharacter, resolvedCharacter, mainCharacter)

  meta.mainAltRole = "ALT"
  meta.mainCharacter = resolvedMain or ""
  metaByCharacter[resolvedCharacter] = meta

  TouchLootListProfile(profile)
  if resolvedMain then
    return true, resolvedMain
  end
  return true, ""
end

local function ClearLootLedgerMainCharacter(profile, characterKey)
  local resolvedCharacter = TrimText(characterKey)
  if resolvedCharacter == "" then
    return false, "Select a character first."
  end

  local metaByCharacter = EnsureLootLedgerMetaLocal(profile)
  local meta = type(metaByCharacter[resolvedCharacter]) == "table" and metaByCharacter[resolvedCharacter] or { penaltyPoints = 0 }
  meta.mainAltRole = "MAIN"
  meta.mainCharacter = ""
  metaByCharacter[resolvedCharacter] = meta
  TouchLootListProfile(profile)
  return true
end

local function DeleteLootLedgerCharacter(profile, characterKey)
  if type(profile) ~= "table" then
    return false, "No Loot List profile is available."
  end

  local resolvedCharacter = TrimText(characterKey)
  if resolvedCharacter == "" then
    return false, "Select a character first."
  end

  profile.lootLedger = type(profile.lootLedger) == "table" and profile.lootLedger or {}
  local metaByCharacter = EnsureLootLedgerMetaLocal(profile)
  local view = EnsureLootLedgerViewLocal(profile)
  local removed = false

  if type(profile.lootLedger[resolvedCharacter]) == "table" then
    profile.lootLedger[resolvedCharacter] = nil
    removed = true
  end
  if type(metaByCharacter[resolvedCharacter]) == "table" then
    metaByCharacter[resolvedCharacter] = nil
    removed = true
  end
  if type(view.collapsedGroups) == "table" and view.collapsedGroups[resolvedCharacter] ~= nil then
    view.collapsedGroups[resolvedCharacter] = nil
    removed = true
  end
  if RemoveCharacterFromLootListGroups(profile, resolvedCharacter) then
    removed = true
  end

  local deletedLower = string.lower(resolvedCharacter)
  for _, meta in pairs(metaByCharacter) do
    if type(meta) == "table" and string.lower(TrimText(meta.mainCharacter)) == deletedLower then
      meta.mainCharacter = ""
      meta.mainAltRole = "MAIN"
      removed = true
    end
  end

  if not removed then
    return false, "Character not found in the Loot List."
  end

  TouchLootListProfile(profile)
  return true
end

local function SeedLootLedgerTestRoster(profile, profileName)
  if type(profile) ~= "table" then
    return false, "No Loot List profile is available."
  end
  if tonumber(profile.__lootLedgerDemoSeedVersion or 0) >= LOOT_LEDGER_TEST_ROSTER_VERSION then
    return false, "Test raiders are already seeded in this profile."
  end

  local metaByCharacter = EnsureLootLedgerMetaLocal(profile)
  profile.lootLedger = type(profile.lootLedger) == "table" and profile.lootLedger or {}
  local addedCount = 0

  for _, info in ipairs(LOOT_LEDGER_TEST_ROSTER) do
    local characterKey = TrimText(info.character)
    if characterKey ~= "" then
      local existedBefore = type(metaByCharacter[characterKey]) == "table" or type(profile.lootLedger[characterKey]) == "table"
      local meta = type(metaByCharacter[characterKey]) == "table" and metaByCharacter[characterKey] or { penaltyPoints = 0 }
      meta.className = TrimText(info.className)
      meta.classFile = TrimText(info.classFile)
      meta.specName = TrimText(info.specName)
      meta.specIcon = TrimText(info.specIcon)
      meta.note = TrimText(info.note)
      meta.mainAltRole = NormalizeLootLedgerRoleLocal(info.mainAltRole)
      meta.mainCharacter = meta.mainAltRole == "ALT" and TrimText(info.mainCharacter) or ""
      meta.penaltyPoints = tonumber(meta.penaltyPoints) or 0
      metaByCharacter[characterKey] = meta

      profile.lootLedger[characterKey] = profile.lootLedger[characterKey] or {
        character = characterKey,
        totalPoints = 0,
        items = {},
      }
      profile.lootLedger[characterKey].items = type(profile.lootLedger[characterKey].items) == "table" and profile.lootLedger[characterKey].items or {}

      local demoItem = type(info.demoItem) == "table" and info.demoItem or nil
      if demoItem and #(profile.lootLedger[characterKey].items or {}) == 0 then
        AddLootLedgerEntry(
          profileName,
          characterKey,
          demoItem.name,
          demoItem.itemLevel,
          demoItem.points,
          "Demo roster seed"
        )
      end
      if not existedBefore then
        addedCount = addedCount + 1
      end
    end
  end

  profile.__lootLedgerDemoSeedVersion = LOOT_LEDGER_TEST_ROSTER_VERSION
  TouchLootListProfile(profile)
  return true, string.format(
    "Added %d test raiders to profile \"%s\". %d sample raiders are available in total.",
    addedCount,
    tostring(profileName or ""),
    #LOOT_LEDGER_TEST_ROSTER
  )
end

local function EnsureLootListGroupsLocal(profile)
  profile = type(profile) == "table" and profile or {}
  profile.lootListGroups = profile.lootListGroups or {}
  local normalized = {}
  local seen = {}
  for _, group in ipairs(profile.lootListGroups) do
    if type(group) == "table" then
      local groupName = TrimText(group.name)
      local groupKey = string.lower(groupName)
      if groupName ~= "" and not seen[groupKey] then
        seen[groupKey] = true
        local members = {}
        local memberSeen = {}
        for _, memberName in ipairs(group.members or {}) do
          local resolvedMember = TrimText(memberName)
          local memberKey = string.lower(resolvedMember)
          if resolvedMember ~= "" and not memberSeen[memberKey] then
            memberSeen[memberKey] = true
            members[#members + 1] = resolvedMember
          end
        end
        table.sort(members, function(a, b)
          return string.lower(tostring(a)) < string.lower(tostring(b))
        end)
        normalized[#normalized + 1] = {
          name = groupName,
          collapsed = group.collapsed == true,
          members = members,
        }
      end
    end
  end
  table.sort(normalized, function(a, b)
    return string.lower(tostring(a.name or "")) < string.lower(tostring(b.name or ""))
  end)
  profile.lootListGroups = normalized
  return normalized
end

local function FindLootListGroupIndexLocal(groups, groupName)
  local needle = string.lower(TrimText(groupName))
  if needle == "" then
    return nil
  end
  for index, group in ipairs(groups or {}) do
    if string.lower(tostring(group and group.name or "")) == needle then
      return index
    end
  end
  return nil
end

function RemoveCharacterFromLootListGroups(profile, characterKey)
  local groups = EnsureLootListGroupsLocal(profile)
  local resolvedCharacter = TrimText(characterKey)
  if resolvedCharacter == "" then
    return nil
  end
  local removedFrom = nil
  for _, group in ipairs(groups) do
    for index = #group.members, 1, -1 do
      if group.members[index] == resolvedCharacter then
        removedFrom = removedFrom or group.name
        table.remove(group.members, index)
      end
    end
  end
  return removedFrom
end

local function AssignCharacterToLootListGroup(profile, characterKey, groupName)
  local resolvedCharacter = TrimText(characterKey)
  local resolvedGroup = TrimText(groupName)
  if resolvedCharacter == "" then
    return false, "Character-Realm is required."
  end
  local groups = EnsureLootListGroupsLocal(profile)
  RemoveCharacterFromLootListGroups(profile, resolvedCharacter)
  if resolvedGroup == "" then
    TouchLootListProfile(profile)
    return true
  end
  local groupIndex = FindLootListGroupIndexLocal(groups, resolvedGroup)
  if not groupIndex then
    return false, "Loot List group not found."
  end
  local group = groups[groupIndex]
  group.collapsed = false
  group.members = group.members or {}
  group.members[#group.members + 1] = resolvedCharacter
  table.sort(group.members, function(a, b)
    return string.lower(tostring(a)) < string.lower(tostring(b))
  end)
  TouchLootListProfile(profile)
  return true
end

local function CreateLootListGroup(profile, groupName)
  local resolvedName = TrimText(groupName)
  if resolvedName == "" then
    return false, "Please enter a group name."
  end
  if #resolvedName > MAX_LOOTLIST_GROUP_NAME then
    resolvedName = resolvedName:sub(1, MAX_LOOTLIST_GROUP_NAME)
  end
  local groups = EnsureLootListGroupsLocal(profile)
  if FindLootListGroupIndexLocal(groups, resolvedName) then
    return false, "Loot List group already exists."
  end
  groups[#groups + 1] = {
    name = resolvedName,
    collapsed = false,
    members = {},
  }
  table.sort(groups, function(a, b)
    return string.lower(tostring(a.name or "")) < string.lower(tostring(b.name or ""))
  end)
  TouchLootListProfile(profile)
  return true, resolvedName
end

local function DeleteLootListGroup(profile, groupName)
  local groups = EnsureLootListGroupsLocal(profile)
  local groupIndex = FindLootListGroupIndexLocal(groups, groupName)
  if not groupIndex then
    return false, "Loot List group not found."
  end
  table.remove(groups, groupIndex)
  TouchLootListProfile(profile)
  return true
end

local function ToggleLootListGroupCollapsedLocal(profile, groupName)
  local groups = EnsureLootListGroupsLocal(profile)
  local groupIndex = FindLootListGroupIndexLocal(groups, groupName)
  if not groupIndex then
    return false
  end
  groups[groupIndex].collapsed = not groups[groupIndex].collapsed
  TouchLootListProfile(profile)
  return true
end

local function GetLootListGroupName(profile, characterKey)
  local resolvedCharacter = TrimText(characterKey)
  if resolvedCharacter == "" then
    return nil
  end
  for _, group in ipairs(EnsureLootListGroupsLocal(profile)) do
    for _, memberName in ipairs(group.members or {}) do
      if memberName == resolvedCharacter then
        return group.name
      end
    end
  end
  return nil
end

local function BuildCharacterKeyFromUnit(unit)
  if not unit or type(UnitExists) ~= "function" or not UnitExists(unit) then
    return nil
  end
  local name, realm
  if type(UnitFullName) == "function" then
    name, realm = UnitFullName(unit)
  else
    name = UnitName(unit)
  end
  name = TrimText(name)
  realm = TrimText(realm)
  if name == "" then
    return nil
  end
  if realm ~= "" then
    realm = realm:gsub("%s+", "")
    return name .. "-" .. realm
  end
  return name
end

local function GetLootListProfileRevision(profile)
  if type(GetLootProfileDomainRevision) == "function" then
    return GetLootProfileDomainRevision(profile, "operational")
  end
  if type(profile) ~= "table" then
    return 0
  end
  profile.meta = profile.meta or {}
  return math.max(0, tonumber(profile.meta.revision) or 0)
end

local function GetRosterCacheNow()
  return (type(GetServerTime) == "function" and GetServerTime()) or (time and time() or 0)
end

local function CopyRosterCharacterInfoMap(values)
  local copied = {}
  for characterKey, info in pairs(values or {}) do
    if type(info) == "table" then
      copied[characterKey] = {
        className = TrimText(info.className),
        classFile = TrimText(info.classFile),
        specName = TrimText(info.specName),
        specIcon = TrimText(info.specIcon),
      }
    end
  end
  return copied
end

local function BuildJoinedSessionRosterSignature(activeSession)
  if type(activeSession) ~= "table" then
    return ""
  end
  local keys = {}
  local seen = {}
  for memberKey, member in pairs(activeSession.members or {}) do
    local resolvedKey = TrimText(memberKey ~= "" and memberKey or (type(member) == "table" and member.name or ""))
    local status = TrimText(type(member) == "table" and member.status or "JOINED")
    if resolvedKey ~= "" and status == "JOINED" then
      local lowered = string.lower(resolvedKey)
      if not seen[lowered] then
        seen[lowered] = true
        keys[#keys + 1] = lowered
      end
    end
  end
  local hostKey = string.lower(TrimText(type(activeSession) == "table" and activeSession.host or ""))
  if hostKey ~= "" and not seen[hostKey] then
    keys[#keys + 1] = hostKey
  end
  table.sort(keys)
  return table.concat(keys, "\031")
end

local function BuildRosterCharacterInfoMap(profile, profileName)
  local info = {}
  local metaByCharacter = EnsureLootLedgerMetaLocal(profile)
  local changed = false
  local activeSession, activeProfileName = GetActiveLootSession()
  local allowRosterSync = type(activeSession) == "table"
    and tostring(activeProfileName or activeSession.profileName or "") == tostring(profileName or "")
  local joinedMembers = {}
  local joinedMemberCount = 0

  if not allowRosterSync then
    return info
  end

  local cacheKey = string.lower(TrimText(profileName))
  local cacheNow = GetRosterCacheNow()
  local sessionId = TrimText(type(activeSession) == "table" and activeSession.id or "")
  local joinedSignature = BuildJoinedSessionRosterSignature(activeSession)
  local profileRevision = GetLootListProfileRevision(profile)
  local cached = rosterCharacterInfoCache[cacheKey]
  if type(cached) == "table"
    and tonumber(cached.revision) == profileRevision
    and TrimText(cached.sessionId) == sessionId
    and TrimText(cached.joinedSignature) == joinedSignature
    and (tonumber(cached.expiresAt) or 0) >= cacheNow
  then
    return CopyRosterCharacterInfoMap(cached.info)
  end

  for memberKey, member in pairs(activeSession.members or {}) do
    local resolvedKey = TrimText(memberKey ~= "" and memberKey or (type(member) == "table" and member.name or ""))
    local status = TrimText(type(member) == "table" and member.status or "JOINED")
    if resolvedKey ~= "" and status == "JOINED" then
      local lookupKey = string.lower(resolvedKey)
      if not joinedMembers[lookupKey] then
        joinedMembers[lookupKey] = true
        joinedMemberCount = joinedMemberCount + 1
      end
    end
  end

  local hostKey = TrimText(type(activeSession) == "table" and activeSession.host or "")
  if hostKey ~= "" then
    local lookupKey = string.lower(hostKey)
    if not joinedMembers[lookupKey] then
      joinedMembers[lookupKey] = true
      joinedMemberCount = joinedMemberCount + 1
    end
  end

  if joinedMemberCount == 0 then
    return info
  end

  local function rememberUnit(unit)
    local characterKey = BuildCharacterKeyFromUnit(unit)
    if not characterKey then
      return
    end
    if not joinedMembers[string.lower(characterKey)] then
      return
    end
    local className, classFile = UnitClass(unit)
    className = TrimText(className)
    classFile = TrimText(classFile)
    local specName = ""
    local specIcon = ""
    local specID = nil
    if unit == "player" and type(GetSpecialization) == "function" and type(GetSpecializationInfo) == "function" then
      local specIndex = GetSpecialization()
      if tonumber(specIndex) and specIndex > 0 then
        local resolvedSpecID, localizedSpecName, _, localizedSpecIcon = GetSpecializationInfo(specIndex)
        specID = tonumber(resolvedSpecID)
        specName = TrimText(localizedSpecName)
        specIcon = TrimText(localizedSpecIcon)
      end
    elseif type(GetInspectSpecialization) == "function" and type(GetSpecializationInfoByID) == "function" then
      specID = tonumber(GetInspectSpecialization(unit))
      if tonumber(specID) and specID > 0 then
        local _, localizedSpecName, _, localizedSpecIcon = GetSpecializationInfoByID(specID)
        specName = TrimText(localizedSpecName)
        specIcon = TrimText(localizedSpecIcon)
      end
    end
    if type(GetEnglishClassAndSpec) == "function" then
      className, specName, specID = GetEnglishClassAndSpec(classFile, specID, className, specName, specIcon)
    end

    local meta = type(metaByCharacter[characterKey]) == "table" and metaByCharacter[characterKey] or { penaltyPoints = 0 }
    if meta.className ~= className then
      meta.className = className
      changed = true
    end
    if meta.classFile ~= classFile then
      meta.classFile = classFile
      changed = true
    end
    if specName ~= "" and meta.specName ~= specName then
      meta.specName = specName
      changed = true
    end
    if specID and tonumber(meta.specID) ~= specID then
      meta.specID = specID
      changed = true
    end
    if specIcon ~= "" and meta.specIcon ~= specIcon then
      meta.specIcon = specIcon
      changed = true
    end
    meta.penaltyPoints = tonumber(meta.penaltyPoints) or 0
    metaByCharacter[characterKey] = meta
    info[characterKey] = {
      className = className,
      classFile = classFile,
      specName = TrimText(meta.specName ~= "" and meta.specName or className),
      specIcon = TrimText(meta.specIcon),
    }
  end

  if type(UnitExists) == "function" then
    rememberUnit("player")
    if type(IsInRaid) == "function" and IsInRaid() and type(GetNumGroupMembers) == "function" then
      for index = 1, GetNumGroupMembers() do
        rememberUnit("raid" .. tostring(index))
      end
    elseif type(IsInGroup) == "function" and IsInGroup() and type(GetNumSubgroupMembers) == "function" then
      for index = 1, GetNumSubgroupMembers() do
        rememberUnit("party" .. tostring(index))
      end
    end
  end

  if changed then
    TouchLootListProfile(profile)
  end

  rosterCharacterInfoCache[cacheKey] = {
    revision = GetLootListProfileRevision(profile),
    sessionId = sessionId,
    joinedSignature = joinedSignature,
    expiresAt = cacheNow + ROSTER_INFO_CACHE_TTL_SECONDS,
    info = CopyRosterCharacterInfoMap(info),
  }
  return info
end

local function CopyCharacterList(values)
  local copied = {}
  for index, value in ipairs(values or {}) do
    copied[index] = value
  end
  return copied
end

local function BuildLootLedgerSearchHaystack(row)
  if type(row) ~= "table" then
    return ""
  end
  return string.lower(table.concat({
    tostring(row.character or ""),
    tostring(row.note or ""),
    tostring(row.specName or ""),
    tostring(row.className or ""),
    tostring(row.mainCharacter or ""),
  }, "\031"))
end

local VALID_LOOT_LEDGER_SORT_KEYS = {
  spec = true,
  name = true,
  note = true,
  sessions = true,
  items = true,
  points = true,
  total = true,
  bis = true,
  offspec = true,
  transmog = true,
  disenchant = true,
}

local NUMERIC_LOOT_LEDGER_SORT_KEYS = {
  sessions = true,
  items = true,
  points = true,
  total = true,
  bis = true,
  offspec = true,
  transmog = true,
  disenchant = true,
}

local function NormalizeLootLedgerSortOptions(sortOptions)
  local key = TrimText(type(sortOptions) == "table" and sortOptions.key or "")
  if VALID_LOOT_LEDGER_SORT_KEYS[key] ~= true then
    key = "name"
  end

  local descending = nil
  if type(sortOptions) == "table" then
    descending = sortOptions.descending
  end
  if type(descending) ~= "boolean" then
    descending = NUMERIC_LOOT_LEDGER_SORT_KEYS[key] == true
  end

  return key, descending
end

local function GetLootLedgerSortComparable(row, summaryComponent, sortKey)
  if sortKey == "spec" then
    local specName = TrimText(type(row) == "table" and row.specName or "")
    if specName ~= "" then
      return string.lower(specName)
    end
    return string.lower(TrimText(type(row) == "table" and row.className or ""))
  end
  if sortKey == "name" then
    return string.lower(tostring(type(row) == "table" and row.character or ""))
  end
  if sortKey == "note" then
    return string.lower(TrimText(type(row) == "table" and row.note or ""))
  end
  if sortKey == "sessions" then
    return tonumber(type(row) == "table" and row.sessionCount or 0) or 0
  end
  if sortKey == "items" then
    return tonumber(type(row) == "table" and row.itemCount or 0) or 0
  end
  if sortKey == "points" then
    return tonumber(type(row) == "table" and row.totalPoints or 0) or 0
  end
  if sortKey == "total" then
    return tonumber(type(summaryComponent) == "table" and summaryComponent.totalPoints or nil)
      or tonumber(type(row) == "table" and row.totalPoints or 0)
      or 0
  end
  if sortKey == "bis" then
    return tonumber(type(row) == "table" and row.bisCount or 0) or 0
  end
  if sortKey == "offspec" then
    return tonumber(type(row) == "table" and row.offSpecCount or 0) or 0
  end
  if sortKey == "transmog" then
    return tonumber(type(row) == "table" and row.transmogCount or 0) or 0
  end
  if sortKey == "disenchant" then
    return tonumber(type(row) == "table" and row.disenchantCount or 0) or 0
  end
  return string.lower(tostring(type(row) == "table" and row.character or ""))
end

local function CompareLootLedgerSortRows(rowA, rowB, summaryA, summaryB, sortKey, descending)
  local comparableA = GetLootLedgerSortComparable(rowA, summaryA, sortKey)
  local comparableB = GetLootLedgerSortComparable(rowB, summaryB, sortKey)

  if NUMERIC_LOOT_LEDGER_SORT_KEYS[sortKey] == true then
    local valueA = tonumber(comparableA) or 0
    local valueB = tonumber(comparableB) or 0
    if valueA ~= valueB then
      if descending then
        return valueA > valueB
      end
      return valueA < valueB
    end
  else
    local valueA = tostring(comparableA or "")
    local valueB = tostring(comparableB or "")
    if valueA ~= valueB then
      if descending then
        return valueA > valueB
      end
      return valueA < valueB
    end
  end

  local nameA = string.lower(tostring(type(rowA) == "table" and rowA.character or ""))
  local nameB = string.lower(tostring(type(rowB) == "table" and rowB.character or ""))
  if nameA ~= nameB then
    return nameA < nameB
  end
  return tostring(type(rowA) == "table" and rowA.character or "") < tostring(type(rowB) == "table" and rowB.character or "")
end

local function BuildLootLedgerSummaryComponents(profile, byCharacter)
  local metaByCharacter = EnsureLootLedgerMetaLocal(profile)
  local lowerToCharacter = {}
  local roleByCharacter = {}
  local mainByCharacter = {}
  local rootByCharacter = {}
  local groups = {}
  local componentsByCharacter = {}

  for characterKey in pairs(byCharacter or {}) do
    lowerToCharacter[string.lower(tostring(characterKey))] = characterKey
  end

  for characterKey in pairs(byCharacter or {}) do
    local meta = type(metaByCharacter[characterKey]) == "table" and metaByCharacter[characterKey] or nil
    local role = NormalizeLootLedgerRoleLocal(meta and meta.mainAltRole)
    local target = TrimText(meta and meta.mainCharacter or "")
    local resolvedTarget = nil
    if role == "ALT" and target ~= "" then
      resolvedTarget = lowerToCharacter[string.lower(target)] or nil
      if resolvedTarget == characterKey then
        resolvedTarget = nil
      end
      if resolvedTarget then
        local targetMeta = type(metaByCharacter[resolvedTarget]) == "table" and metaByCharacter[resolvedTarget] or nil
        if NormalizeLootLedgerRoleLocal(targetMeta and targetMeta.mainAltRole) ~= "MAIN" then
          resolvedTarget = nil
        end
      end
    end
    roleByCharacter[characterKey] = role
    mainByCharacter[characterKey] = resolvedTarget or ""
  end

  for characterKey in pairs(byCharacter or {}) do
    local role = roleByCharacter[characterKey] or "MAIN"
    local resolvedMain = mainByCharacter[characterKey] or ""
    local rootKey = (role == "ALT" and resolvedMain ~= "") and resolvedMain or characterKey
    rootByCharacter[characterKey] = rootKey
    groups[rootKey] = groups[rootKey] or {
      rootCharacter = rootKey,
      members = {},
      memberCount = 0,
      itemCount = 0,
      sessionCount = 0,
      totalPoints = 0,
      bisCount = 0,
      offSpecCount = 0,
      transmogCount = 0,
      disenchantCount = 0,
      penaltyPoints = 0,
    }
    groups[rootKey].members[#groups[rootKey].members + 1] = characterKey
  end

  for _, component in pairs(groups) do
    table.sort(component.members, function(a, b)
      if tostring(a or "") == tostring(component.rootCharacter or "") then
        return true
      end
      if tostring(b or "") == tostring(component.rootCharacter or "") then
        return false
      end
      return string.lower(tostring(a or "")) < string.lower(tostring(b or ""))
    end)
    component.memberCount = #component.members
    for _, memberKey in ipairs(component.members) do
      local source = byCharacter[memberKey]
      if source then
        component.itemCount = component.itemCount + (tonumber(source.itemCount) or 0)
        component.sessionCount = component.sessionCount + (tonumber(source.sessionCount) or 0)
        component.totalPoints = component.totalPoints + (tonumber(source.totalPoints) or 0)
        component.bisCount = component.bisCount + (tonumber(source.bisCount) or 0)
        component.offSpecCount = component.offSpecCount + (tonumber(source.offSpecCount) or 0)
        component.transmogCount = component.transmogCount + (tonumber(source.transmogCount) or 0)
        component.disenchantCount = component.disenchantCount + (tonumber(source.disenchantCount) or 0)
        component.penaltyPoints = component.penaltyPoints + (tonumber(source.penaltyPoints) or 0)
      end
    end
    for _, memberKey in ipairs(component.members) do
      componentsByCharacter[memberKey] = component
    end
  end

  return componentsByCharacter, roleByCharacter, mainByCharacter, rootByCharacter, groups
end

local function BuildLootLedgerNavRows(profile, profileName, searchText, sortOptions)
  local baseRows = GetLootLedgerCharacters(profileName)
  local rosterInfo = BuildRosterCharacterInfoMap(profile, profileName)
  local byCharacter = {}
  local orderedCharacters = {}
  local sortKey, sortDescending = NormalizeLootLedgerSortOptions(sortOptions)
  for _, row in ipairs(baseRows) do
    local info = rosterInfo[row.character] or row
    row.classFile = TrimText(info.classFile or row.classFile)
    row.className = TrimText(info.className or row.className)
    row.specName = TrimText(info.specName or row.specName)
    row.specIcon = TrimText(info.specIcon or row.specIcon)
    row.note = TrimText(info.note or row.note)
    byCharacter[row.character] = row
    orderedCharacters[#orderedCharacters + 1] = row
  end

  table.sort(orderedCharacters, function(a, b)
    return string.lower(tostring(a.character or "")) < string.lower(tostring(b.character or ""))
  end)

  local componentByCharacter, roleByCharacter, mainByCharacter, rootByCharacter, groups = BuildLootLedgerSummaryComponents(profile, byCharacter)

  local filterText = string.lower(TrimText(searchText))
  local rows = {}
  local orderedRoots = {}
  for rootKey in pairs(groups or {}) do
    orderedRoots[#orderedRoots + 1] = rootKey
  end
  table.sort(orderedRoots, function(a, b)
    local rowA = byCharacter[a]
    local rowB = byCharacter[b]
    if not rowA or not rowB then
      return string.lower(tostring(a or "")) < string.lower(tostring(b or ""))
    end
    return CompareLootLedgerSortRows(
      rowA,
      rowB,
      componentByCharacter[a] or groups[a],
      componentByCharacter[b] or groups[b],
      sortKey,
      sortDescending
    )
  end)

  for _, rootKey in ipairs(orderedRoots) do
    local rootRow = byCharacter[rootKey]
    local summaryComponent = componentByCharacter[rootKey] or groups[rootKey]
    if rootRow and summaryComponent then
      local rootMatches = filterText == "" or string.find(BuildLootLedgerSearchHaystack(rootRow), filterText, 1, true) ~= nil
      local matchingChildren = {}
      for _, memberKey in ipairs(summaryComponent.members or {}) do
        if memberKey ~= rootKey then
          local childRow = byCharacter[memberKey]
          if childRow and (filterText == "" or string.find(BuildLootLedgerSearchHaystack(childRow), filterText, 1, true) ~= nil) then
            matchingChildren[#matchingChildren + 1] = memberKey
          end
        end
      end

      if filterText == "" or rootMatches or #matchingChildren > 0 then
        local collapsed = IsLootLedgerGroupCollapsedLocal(profile, rootKey, summaryComponent.memberCount)
        local hasChildren = tonumber(summaryComponent.memberCount) and tonumber(summaryComponent.memberCount) > 1
        local showChildren = hasChildren and ((filterText ~= "" and (rootMatches or #matchingChildren > 0)) or not collapsed)
        rows[#rows + 1] = {
          type = "character",
          key = "character:" .. string.lower(tostring(rootRow.character or "")),
          character = rootRow.character,
          specName = rootRow.specName,
          specIcon = rootRow.specIcon,
          className = rootRow.className,
          classFile = rootRow.classFile,
          note = rootRow.note,
          mainAltRole = roleByCharacter[rootRow.character] or NormalizeLootLedgerRoleLocal(rootRow.mainAltRole),
          mainCharacter = mainByCharacter[rootRow.character] or "",
          rootCharacter = rootKey,
          depth = 0,
          collapsed = collapsed,
          isExpandable = hasChildren,
          isGroupRoot = true,
          itemCount = tonumber(summaryComponent.itemCount) or (tonumber(rootRow.itemCount) or 0),
          sessionCount = tonumber(summaryComponent.sessionCount) or (tonumber(rootRow.sessionCount) or 0),
          totalPoints = tonumber(summaryComponent.totalPoints) or (tonumber(rootRow.totalPoints) or 0),
          bisCount = tonumber(summaryComponent.bisCount) or (tonumber(rootRow.bisCount) or 0),
          offSpecCount = tonumber(summaryComponent.offSpecCount) or (tonumber(rootRow.offSpecCount) or 0),
          transmogCount = tonumber(summaryComponent.transmogCount) or (tonumber(rootRow.transmogCount) or 0),
          disenchantCount = tonumber(summaryComponent.disenchantCount) or (tonumber(rootRow.disenchantCount) or 0),
          penaltyPoints = tonumber(summaryComponent.penaltyPoints) or (tonumber(rootRow.penaltyPoints) or 0),
          ownItemCount = tonumber(rootRow.itemCount) or 0,
          ownSessionCount = tonumber(rootRow.sessionCount) or 0,
          ownTotalPoints = tonumber(rootRow.totalPoints) or 0,
          ownBisCount = tonumber(rootRow.bisCount) or 0,
          ownOffSpecCount = tonumber(rootRow.offSpecCount) or 0,
          ownTransmogCount = tonumber(rootRow.transmogCount) or 0,
          ownDisenchantCount = tonumber(rootRow.disenchantCount) or 0,
          ownPenaltyPoints = tonumber(rootRow.penaltyPoints) or 0,
          displayItemCount = tonumber(rootRow.itemCount) or 0,
          displaySessionCount = tonumber(rootRow.sessionCount) or 0,
          displayTotalPoints = tonumber(rootRow.totalPoints) or 0,
          displayBisCount = tonumber(rootRow.bisCount) or 0,
          displayOffSpecCount = tonumber(rootRow.offSpecCount) or 0,
          displayTransmogCount = tonumber(rootRow.transmogCount) or 0,
          displayDisenchantCount = tonumber(rootRow.disenchantCount) or 0,
          displayPenaltyPoints = tonumber(rootRow.penaltyPoints) or 0,
          groupItemCount = tonumber(summaryComponent.itemCount) or (tonumber(rootRow.itemCount) or 0),
          groupSessionCount = tonumber(summaryComponent.sessionCount) or (tonumber(rootRow.sessionCount) or 0),
          groupTotalPoints = tonumber(summaryComponent.totalPoints) or (tonumber(rootRow.totalPoints) or 0),
          groupBisCount = tonumber(summaryComponent.bisCount) or (tonumber(rootRow.bisCount) or 0),
          groupOffSpecCount = tonumber(summaryComponent.offSpecCount) or (tonumber(rootRow.offSpecCount) or 0),
          groupTransmogCount = tonumber(summaryComponent.transmogCount) or (tonumber(rootRow.transmogCount) or 0),
          groupDisenchantCount = tonumber(summaryComponent.disenchantCount) or (tonumber(rootRow.disenchantCount) or 0),
          groupPenaltyPoints = tonumber(summaryComponent.penaltyPoints) or (tonumber(rootRow.penaltyPoints) or 0),
          summaryMembers = CopyCharacterList(summaryComponent.members or { rootRow.character }),
          summaryMemberCount = tonumber(summaryComponent.memberCount) or 1,
        }

        if showChildren then
          local childKeys = {}
          for _, memberKey in ipairs(summaryComponent.members or {}) do
            if memberKey ~= rootKey then
              childKeys[#childKeys + 1] = memberKey
            end
          end
          table.sort(childKeys, function(a, b)
            local rowA = byCharacter[a]
            local rowB = byCharacter[b]
            if not rowA or not rowB then
              return string.lower(tostring(a or "")) < string.lower(tostring(b or ""))
            end
            return CompareLootLedgerSortRows(
              rowA,
              rowB,
              summaryComponent,
              summaryComponent,
              sortKey,
              sortDescending
            )
          end)

          for _, memberKey in ipairs(childKeys) do
            local childRow = byCharacter[memberKey]
            if childRow then
              local childMatches = filterText == "" or rootMatches or string.find(BuildLootLedgerSearchHaystack(childRow), filterText, 1, true) ~= nil
              if childMatches then
                rows[#rows + 1] = {
                  type = "character",
                  key = "character:" .. string.lower(tostring(childRow.character or "")),
                  character = childRow.character,
                  specName = childRow.specName,
                  specIcon = childRow.specIcon,
                  className = childRow.className,
                  classFile = childRow.classFile,
                  note = childRow.note,
                  mainAltRole = roleByCharacter[childRow.character] or NormalizeLootLedgerRoleLocal(childRow.mainAltRole),
                  mainCharacter = mainByCharacter[childRow.character] or "",
                  rootCharacter = rootByCharacter[childRow.character] or rootKey,
                  depth = 1,
                  collapsed = false,
                  isExpandable = false,
                  isGroupRoot = false,
                  itemCount = tonumber(childRow.itemCount) or 0,
                  sessionCount = tonumber(childRow.sessionCount) or 0,
                  totalPoints = tonumber(childRow.totalPoints) or 0,
                  bisCount = tonumber(childRow.bisCount) or 0,
                  offSpecCount = tonumber(childRow.offSpecCount) or 0,
                  transmogCount = tonumber(childRow.transmogCount) or 0,
                  disenchantCount = tonumber(childRow.disenchantCount) or 0,
                  penaltyPoints = tonumber(childRow.penaltyPoints) or 0,
                  ownItemCount = tonumber(childRow.itemCount) or 0,
                  ownSessionCount = tonumber(childRow.sessionCount) or 0,
                  ownTotalPoints = tonumber(childRow.totalPoints) or 0,
                  ownBisCount = tonumber(childRow.bisCount) or 0,
                  ownOffSpecCount = tonumber(childRow.offSpecCount) or 0,
                  ownTransmogCount = tonumber(childRow.transmogCount) or 0,
                  ownDisenchantCount = tonumber(childRow.disenchantCount) or 0,
                  ownPenaltyPoints = tonumber(childRow.penaltyPoints) or 0,
                  displayItemCount = tonumber(childRow.itemCount) or 0,
                  displaySessionCount = tonumber(childRow.sessionCount) or 0,
                  displayTotalPoints = tonumber(childRow.totalPoints) or 0,
                  displayBisCount = tonumber(childRow.bisCount) or 0,
                  displayOffSpecCount = tonumber(childRow.offSpecCount) or 0,
                  displayTransmogCount = tonumber(childRow.transmogCount) or 0,
                  displayDisenchantCount = tonumber(childRow.disenchantCount) or 0,
                  displayPenaltyPoints = tonumber(childRow.penaltyPoints) or 0,
                  groupItemCount = tonumber(summaryComponent.itemCount) or (tonumber(childRow.itemCount) or 0),
                  groupSessionCount = tonumber(summaryComponent.sessionCount) or (tonumber(childRow.sessionCount) or 0),
                  groupTotalPoints = tonumber(summaryComponent.totalPoints) or (tonumber(childRow.totalPoints) or 0),
                  groupBisCount = tonumber(summaryComponent.bisCount) or (tonumber(childRow.bisCount) or 0),
                  groupOffSpecCount = tonumber(summaryComponent.offSpecCount) or (tonumber(childRow.offSpecCount) or 0),
                  groupTransmogCount = tonumber(summaryComponent.transmogCount) or (tonumber(childRow.transmogCount) or 0),
                  groupDisenchantCount = tonumber(summaryComponent.disenchantCount) or (tonumber(childRow.disenchantCount) or 0),
                  groupPenaltyPoints = tonumber(summaryComponent.penaltyPoints) or (tonumber(childRow.penaltyPoints) or 0),
                  summaryMembers = { childRow.character },
                  summaryMemberCount = 1,
                }
              end
            end
          end
        end
      end
    end
  end
  return rows, byCharacter, rootByCharacter
end

INTERNAL_API.LIDCOUNCIL_LootListData_Impl = {
  MAX_LOOTLIST_GROUP_NAME = MAX_LOOTLIST_GROUP_NAME,
  LOOT_LEDGER_TEST_ROSTER_VERSION = LOOT_LEDGER_TEST_ROSTER_VERSION,
  TouchLootListProfile = TouchLootListProfile,
  EnsureLootLedgerMetaLocal = EnsureLootLedgerMetaLocal,
  EnsureLootLedgerViewLocal = EnsureLootLedgerViewLocal,
  IsLootLedgerGroupCollapsedLocal = IsLootLedgerGroupCollapsedLocal,
  ToggleLootLedgerGroupCollapsedLocal = ToggleLootLedgerGroupCollapsedLocal,
  NormalizeLootLedgerRoleLocal = NormalizeLootLedgerRoleLocal,
  ResolveLootLedgerMainTarget = ResolveLootLedgerMainTarget,
  SetLootLedgerMainAltRole = SetLootLedgerMainAltRole,
  SetLootLedgerCharacterNote = SetLootLedgerCharacterNote,
  SetLootLedgerMainCharacter = SetLootLedgerMainCharacter,
  ClearLootLedgerMainCharacter = ClearLootLedgerMainCharacter,
  DeleteLootLedgerCharacter = DeleteLootLedgerCharacter,
  SeedLootLedgerTestRoster = SeedLootLedgerTestRoster,
  EnsureLootListGroupsLocal = EnsureLootListGroupsLocal,
  FindLootListGroupIndexLocal = FindLootListGroupIndexLocal,
  RemoveCharacterFromLootListGroups = RemoveCharacterFromLootListGroups,
  AssignCharacterToLootListGroup = AssignCharacterToLootListGroup,
  CreateLootListGroup = CreateLootListGroup,
  DeleteLootListGroup = DeleteLootListGroup,
  ToggleLootListGroupCollapsedLocal = ToggleLootListGroupCollapsedLocal,
  GetLootListGroupName = GetLootListGroupName,
  BuildCharacterKeyFromUnit = BuildCharacterKeyFromUnit,
  BuildRosterCharacterInfoMap = BuildRosterCharacterInfoMap,
  CopyCharacterList = CopyCharacterList,
  BuildLootLedgerSearchHaystack = BuildLootLedgerSearchHaystack,
  BuildLootLedgerSummaryComponents = BuildLootLedgerSummaryComponents,
  BuildLootLedgerNavRows = BuildLootLedgerNavRows,
}
