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

  local ApplyFrameSkin = deps.ApplyFrameSkin
  local TrimText = deps.TrimText
  local GetSubdomainParentDomain = deps.GetSubdomainParentDomain

  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 ensureMemberState(editor, memberKey)
    editor.lootSessionConflictSelectionState = type(editor.lootSessionConflictSelectionState) == "table"
      and editor.lootSessionConflictSelectionState
      or {}
    local resolvedMemberKey = trim(memberKey)
    editor.lootSessionConflictSelectionState[resolvedMemberKey] = type(editor.lootSessionConflictSelectionState[resolvedMemberKey]) == "table"
      and editor.lootSessionConflictSelectionState[resolvedMemberKey]
      or {}
    return editor.lootSessionConflictSelectionState[resolvedMemberKey]
  end

  local function buildVisibleRows(detailConflict)
    local visible = {}
    for _, leaf in ipairs(type(detailConflict) == "table" and type(detailConflict.leaves) == "table" and detailConflict.leaves or {}) do
      local leafLabel = trim(type(leaf) == "table" and leaf.label or "")
      for _, row in ipairs(type(leaf) == "table" and type(leaf.rows) == "table" and leaf.rows or {}) do
        visible[#visible + 1] = {
          subdomain = trim(type(leaf) == "table" and leaf.subdomain or ""),
          leafLabel = leafLabel ~= "" and leafLabel or trim(type(leaf) == "table" and leaf.subdomain or ""),
          row = row,
        }
      end
    end
    table.sort(visible, function(left, right)
      local leftLabel = string.lower(tostring(left.leafLabel or ""))
      local rightLabel = string.lower(tostring(right.leafLabel or ""))
      if leftLabel ~= rightLabel then
        return leftLabel < rightLabel
      end
      return string.lower(tostring(type(left.row) == "table" and left.row.pathLabel or "")) < string.lower(tostring(type(right.row) == "table" and right.row.pathLabel or ""))
    end)
    return visible
  end

  local function isRecommended(detailConflict)
    local comparison = string.lower(trim(type(detailConflict) == "table" and detailConflict.comparison or ""))
    return comparison == "remote_newer"
  end

  local function applyPreset(editor, memberKey, detailConflict, mode)
    local state = ensureMemberState(editor, memberKey)
    for _, visible in ipairs(buildVisibleRows(detailConflict)) do
      local rowId = trim(type(visible.row) == "table" and visible.row.id or "")
      if rowId ~= "" then
        if mode == "all" then
          state[rowId] = true
        elseif mode == "recommended" then
          state[rowId] = isRecommended(detailConflict)
        else
          state[rowId] = false
        end
      end
    end
  end

  local function ensureRows(frame, count)
    frame.rows = frame.rows or {}
    while #frame.rows < count do
      local row = CreateFrame("Frame", nil, frame.scrollChild)
      row:SetHeight(42)

      local check = CreateFrame("CheckButton", nil, row, "UICheckButtonTemplate")
      check:SetSize(22, 22)
      row.check = check

      local scopeText = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
      scopeText:SetJustifyH("LEFT")
      scopeText:SetTextColor(0.95, 0.85, 0.42, 1.00)
      row.scopeText = scopeText

      local pathText = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
      pathText:SetJustifyH("LEFT")
      pathText:SetTextColor(0.86, 0.88, 0.94, 1.00)
      row.pathText = pathText

      local hostText = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
      hostText:SetJustifyH("LEFT")
      hostText:SetTextColor(0.38, 0.78, 1.00, 1.00)
      row.hostText = hostText

      local memberText = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
      memberText:SetJustifyH("LEFT")
      memberText:SetTextColor(1.00, 0.82, 0.36, 1.00)
      row.memberText = memberText

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

  local function resolveFrameKey(options)
    local key = trim(type(options) == "table" and options.frameKey or "")
    return key ~= "" and key or "lootSessionSyncConflictResolver"
  end

  local function EnsureConflictResolverFrame(editor, parent, options)
    if not editor or not parent then
      return nil
    end
    local frameKey = resolveFrameKey(options)
    if editor[frameKey] then
      return editor[frameKey]
    end

    local frame = CreateFrame("Frame", nil, parent, "BackdropTemplate")
    ApplyFrameSkin(frame)
    if frame.SetBackdropColor then
      frame:SetBackdropColor(0.05, 0.05, 0.07, 0.96)
    end
    frame:Hide()

    local title = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    title:SetPoint("TOPLEFT", 10, -10)
    title:SetText(trim(type(options) == "table" and options.title or "") ~= "" and trim(options.title) or "Nested Preview Resolver")
    frame.title = title

    local hint = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    hint:SetJustifyH("LEFT")
    hint:SetPoint("TOPLEFT", 10, -28)
    hint:SetPoint("TOPRIGHT", -10, -28)
    hint:SetText("")
    frame.hint = hint

    local recommendedBtn = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate")
    recommendedBtn:SetSize(110, 22)
    recommendedBtn:SetText("Recommended")
    frame.recommendedBtn = recommendedBtn

    local allBtn = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate")
    allBtn:SetSize(110, 22)
    allBtn:SetText("Take Member All")
    frame.allBtn = allBtn

    local clearBtn = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate")
    clearBtn:SetSize(70, 22)
    clearBtn:SetText("Clear")
    frame.clearBtn = clearBtn

    local summaryText = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    summaryText:SetJustifyH("LEFT")
    summaryText:SetText("")
    frame.summaryText = summaryText

    local header = CreateFrame("Frame", nil, frame)
    frame.header = header

    local headerScope = header:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    headerScope:SetJustifyH("LEFT")
    headerScope:SetText("Leaf Scope")
    frame.headerScope = headerScope

    local headerPath = header:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    headerPath:SetJustifyH("LEFT")
    headerPath:SetText("What Differs")
    frame.headerPath = headerPath

    local headerHost = header:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    headerHost:SetJustifyH("LEFT")
    headerHost:SetText("Host Value")
    frame.headerHost = headerHost

    local headerMember = header:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    headerMember:SetJustifyH("LEFT")
    headerMember:SetText("Member Value")
    frame.headerMember = headerMember

    local scrollFrame = CreateFrame("ScrollFrame", nil, frame, "UIPanelScrollFrameTemplate")
    frame.scrollFrame = scrollFrame

    local scrollChild = CreateFrame("Frame", nil, scrollFrame)
    scrollChild:SetSize(1, 1)
    scrollFrame:SetScrollChild(scrollChild)
    frame.scrollChild = scrollChild

    editor[frameKey] = frame
    return frame
  end

  local function BuildSelectedConflictResolution(editor, candidate, detailConflict)
    candidate = type(candidate) == "table" and candidate or {}
    local memberKey = trim(candidate.memberKey)
    local state = ensureMemberState(editor, memberKey)
    local selectionsBySubdomain = {}
    local selectedCount = 0
    for _, visible in ipairs(buildVisibleRows(detailConflict)) do
      local row = type(visible.row) == "table" and visible.row or {}
      local rowId = trim(row.id)
      if rowId ~= "" and state[rowId] == true then
        selectionsBySubdomain[visible.subdomain] = selectionsBySubdomain[visible.subdomain] or {}
        selectionsBySubdomain[visible.subdomain][#selectionsBySubdomain[visible.subdomain] + 1] = rowId
        selectedCount = selectedCount + 1
      end
    end
    return {
      selectionsBySubdomain = selectionsBySubdomain,
      selectedCount = selectedCount,
      selectionActive = selectedCount > 0,
      targetMemberKey = memberKey,
    }
  end

  local function BuildSelectedHostSessionHistoryRepair(editor, candidate, detailConflict, opts)
    candidate = type(candidate) == "table" and candidate or {}
    opts = type(opts) == "table" and opts or {}
    local memberKey = trim(candidate.memberKey)
    local state = ensureMemberState(editor, memberKey)
    local subdomains = {}
    local subdomainLookup = {}
    local selectedCount = 0
    local incompatibleCount = 0

    for _, visible in ipairs(buildVisibleRows(detailConflict)) do
      local row = type(visible.row) == "table" and visible.row or {}
      local rowId = trim(row.id)
      if rowId ~= "" and state[rowId] == true then
        selectedCount = selectedCount + 1
        local subdomain = trim(visible.subdomain)
        local domain = type(GetSubdomainParentDomain) == "function"
          and string.lower(trim(GetSubdomainParentDomain(subdomain)))
          or ""
        -- Session records and saved live-board snapshots are historical host-owned data.
        if domain == "session" and subdomain ~= "" then
          local key = string.lower(subdomain)
          if not subdomainLookup[key] then
            subdomainLookup[key] = true
            subdomains[#subdomains + 1] = subdomain
          end
        else
          incompatibleCount = incompatibleCount + 1
        end
      end
    end

    -- Scope-picker selection is preserved when loading a nested preview.
    -- Use it too, so a host does not have to select the same session leaves twice.
    for _, subdomain in ipairs(type(opts.preselectedSubdomains) == "table" and opts.preselectedSubdomains or {}) do
      local resolvedSubdomain = trim(subdomain)
      local key = string.lower(resolvedSubdomain)
      local domain = type(GetSubdomainParentDomain) == "function"
        and string.lower(trim(GetSubdomainParentDomain(resolvedSubdomain)))
        or ""
      if resolvedSubdomain ~= "" and domain == "session" and not subdomainLookup[key] then
        subdomainLookup[key] = true
        subdomains[#subdomains + 1] = resolvedSubdomain
      end
    end

    return {
      subdomains = subdomains,
      selectedCount = selectedCount,
      compatibleCount = #subdomains,
      incompatibleCount = incompatibleCount,
      selectionActive = selectedCount > 0,
      targetMemberKey = memberKey,
    }
  end

  local function RefreshSyncConflictResolver(editor, parent, candidate, detailConflict, options)
    local frame = EnsureConflictResolverFrame(editor, parent, options)
    if not frame then
      return nil
    end

    candidate = type(candidate) == "table" and candidate or {}
    detailConflict = type(detailConflict) == "table" and detailConflict or nil
    if type(detailConflict) ~= "table"
      or trim(detailConflict.memberKey) == ""
      or trim(detailConflict.memberKey) ~= trim(candidate.memberKey)
      or type(detailConflict.leaves) ~= "table"
      or #detailConflict.leaves == 0 then
      frame:Hide()
      return frame
    end

    if frame.title then
      local titleText = trim(type(options) == "table" and options.title or "")
      frame.title:SetText(titleText ~= "" and titleText or "Nested Preview Resolver")
    end

    local memberKey = trim(candidate.memberKey)
    local state = ensureMemberState(editor, memberKey)
    local visibleRows = buildVisibleRows(detailConflict)
    ensureRows(frame, #visibleRows)

    if frame.hint then
      frame.hint:SetText("What Differs shows the exact field inside the selected leaf, such as one fun-award roll entry, one live-board vote, one selection, or one session member record. Apply + Sync Selected takes checked Member Values. Keep Host Session Data + Sync instead sends only checked Session Records and saved Live Board history from the host.")
    end

    if frame.recommendedBtn then
      frame.recommendedBtn:SetScript("OnClick", function()
        applyPreset(editor, memberKey, detailConflict, "recommended")
        RefreshSyncConflictResolver(editor, parent, candidate, detailConflict, options)
      end)
    end
    if frame.allBtn then
      frame.allBtn:SetScript("OnClick", function()
        applyPreset(editor, memberKey, detailConflict, "all")
        RefreshSyncConflictResolver(editor, parent, candidate, detailConflict, options)
      end)
    end
    if frame.clearBtn then
      frame.clearBtn:SetScript("OnClick", function()
        applyPreset(editor, memberKey, detailConflict, "clear")
        RefreshSyncConflictResolver(editor, parent, candidate, detailConflict, options)
      end)
    end

    local selectedCount = 0
    for index, visible in ipairs(visibleRows) do
      local row = frame.rows[index]
      local detailRow = type(visible.row) == "table" and visible.row or {}
      local rowId = trim(detailRow.id)
      if state[rowId] == nil and isRecommended(detailConflict) then
        state[rowId] = true
      end
      if state[rowId] == true then
        selectedCount = selectedCount + 1
      end

      row:Show()
      row:ClearAllPoints()
      row:SetPoint("TOPLEFT", 0, -((index - 1) * 44))
      row:SetPoint("TOPRIGHT", -18, -((index - 1) * 44))

      row.check:ClearAllPoints()
      row.check:SetPoint("TOPLEFT", 0, -8)
      row.check:SetChecked(state[rowId] == true)
      row.check:SetScript("OnClick", function(self)
        state[rowId] = self:GetChecked() and true or false
        RefreshSyncConflictResolver(editor, parent, candidate, detailConflict, options)
      end)

      row.scopeText:ClearAllPoints()
      row.scopeText:SetPoint("TOPLEFT", 26, -2)
      row.scopeText:SetWidth(130)
      row.scopeText:SetText(tostring(visible.leafLabel or "-"))

      row.pathText:ClearAllPoints()
      row.pathText:SetPoint("TOPLEFT", 160, -2)
      row.pathText:SetWidth(240)
      row.pathText:SetText(tostring(detailRow.pathLabel or "-"))

      row.hostText:ClearAllPoints()
      row.hostText:SetPoint("TOPLEFT", 405, -2)
      row.hostText:SetWidth(150)
      row.hostText:SetText(tostring(detailRow.hostSummary or "-"))

      row.memberText:ClearAllPoints()
      row.memberText:SetPoint("TOPLEFT", 560, -2)
      row.memberText:SetWidth(150)
      row.memberText:SetText(tostring(detailRow.memberSummary or "-"))
    end

    for index = #visibleRows + 1, #frame.rows do
      frame.rows[index]:Hide()
    end

    frame.scrollChild:SetHeight(math.max(1, #visibleRows * 44))
    frame.scrollChild:SetWidth(700)
    frame:SetHeight(math.max(180, tonumber(type(options) == "table" and options.frameHeight) or 250))
    frame:ClearAllPoints()
    if trim(type(options) == "table" and options.layout or "") == "inline" then
      frame:SetPoint("TOPLEFT", parent, "TOPLEFT", 0, 0)
      frame:SetPoint("TOPRIGHT", parent, "TOPRIGHT", 0, 0)
    else
      frame:SetPoint("BOTTOMLEFT", parent, "BOTTOMLEFT", 14, 48)
      frame:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", -14, 48)
    end

    frame.recommendedBtn:ClearAllPoints()
    frame.recommendedBtn:SetPoint("TOPLEFT", 10, -48)
    frame.allBtn:ClearAllPoints()
    frame.allBtn:SetPoint("LEFT", frame.recommendedBtn, "RIGHT", 6, 0)
    frame.clearBtn:ClearAllPoints()
    frame.clearBtn:SetPoint("LEFT", frame.allBtn, "RIGHT", 6, 0)

    frame.summaryText:ClearAllPoints()
    frame.summaryText:SetPoint("LEFT", frame.clearBtn, "RIGHT", 10, 0)
    frame.summaryText:SetText(tostring(selectedCount) .. " / " .. tostring(#visibleRows) .. " preview rows selected. Use Member Apply for member values or Host Session Sync for selected historical session leaves.")

    frame.header:ClearAllPoints()
    frame.header:SetPoint("TOPLEFT", 10, -78)
    frame.header:SetPoint("TOPRIGHT", -26, -78)
    frame.header:SetHeight(18)
    frame.headerScope:ClearAllPoints()
    frame.headerScope:SetPoint("TOPLEFT", 26, 0)
    frame.headerPath:ClearAllPoints()
    frame.headerPath:SetPoint("TOPLEFT", 160, 0)
    frame.headerHost:ClearAllPoints()
    frame.headerHost:SetPoint("TOPLEFT", 405, 0)
    frame.headerMember:ClearAllPoints()
    frame.headerMember:SetPoint("TOPLEFT", 560, 0)

    frame.scrollFrame:ClearAllPoints()
    frame.scrollFrame:SetPoint("TOPLEFT", 10, -98)
    frame.scrollFrame:SetPoint("BOTTOMRIGHT", -28, 10)

    frame:Show()
    return frame
  end

  return {
    EnsureConflictResolverFrame = EnsureConflictResolverFrame,
    RefreshSyncConflictResolver = RefreshSyncConflictResolver,
    BuildSelectedConflictResolution = BuildSelectedConflictResolution,
    BuildSelectedHostSessionHistoryRepair = BuildSelectedHostSessionHistoryRepair,
  }
end
