-- Modules/Groups.lua
-- Group management module

local ADDON_NAME, BFL = ...
local Groups = BFL:RegisterModule("Groups", {})

-- Built-in groups
local builtinGroups = {
	favorites = {
		id = "favorites",
		name = "Favorites",
		collapsed = false,
		builtin = true,
		order = 1,
		color = { r = 1.0, g = 0.82, b = 0.0 }, -- Gold
		icon = "Interface\\FriendsFrame\\Battlenet-Battleneticon",
	},
	ingame = {
		id = "ingame",
		name = "In-Game",
		collapsed = false,
		builtin = true,
		order = 2,
		color = { r = 1.0, g = 0.82, b = 0.0 }, -- Gold
		icon = "Interface\\Icons\\Inv_misc_groupneedmore",
	},
	recentlyadded = {
		id = "recentlyadded",
		name = "Recently Added",
		collapsed = false,
		builtin = true,
		order = 3,
		color = { r = 1.0, g = 0.82, b = 0.0 }, -- Gold (default)
		icon = "Interface\\FriendsFrame\\UI-Toast-FriendOnlineIcon",
	},
	nogroup = {
		id = "nogroup",
		name = "No Group",
		collapsed = false,
		builtin = true,
		order = 999,
		color = { r = 0.5, g = 0.5, b = 0.5 }, -- Gray
		icon = "Interface\\FriendsFrame\\UI-Toast-FriendOnlineIcon",
	},
}

-- All groups (built-in + custom)
Groups.groups = {}

-- Migrate old bnetAccountID-based UIDs to battleTag-based UIDs
function Groups:MigrateFriendAssignments()
	local DB = BFL:GetModule("DB")
	if not DB then
		return
	end
	local friendGroups = DB:GetFriendGroups() -- Get ALL friendGroups
	if not friendGroups then
		return
	end

	-- Check if Battle.net migration has already been done
	local bnetMigrationDone = DB:Get("bnetUIDMigrationDone_v2") -- Changed flag name to force re-run
	local wowMigrationDone = DB:Get("wowUIDMigrationDone_v1") -- New migration for WoW friends

	if bnetMigrationDone and wowMigrationDone then
		return -- Already migrated
	end

	-- Count old-style UIDs
	local oldBnetUIDs = {}
	local wowUIDsToMigrate = {}
	local totalMappings = 0

	for uid, groups in pairs(friendGroups) do
		-- Skip numeric array indices (corrupted data)
		if type(uid) == "string" then
			totalMappings = totalMappings + 1

			-- Battle.net: Old format "bnet_12345" where 12345 is numeric
			if not bnetMigrationDone and uid:match("^bnet_%d+$") then
				table.insert(oldBnetUIDs, uid)
			end

			-- WoW: Old format "wow_Name" without realm
			if not wowMigrationDone and uid:match("^wow_[^%-]+$") then
				-- Extract character name (without "wow_" prefix)
				local charName = uid:sub(5)
				-- Only migrate if name doesn't look like it already has realm (no "-")
				if not charName:find("-") then
					table.insert(wowUIDsToMigrate, { oldUID = uid, charName = charName, groups = groups })
				end
			end
		end
	end

	-- Debug output
	-- Use BFL:DebugPrint to avoid spamming the user on every reload
	BFL:DebugPrint(BFL.L.MIGRATION_DEBUG_TOTAL .. " " .. totalMappings)
	if not bnetMigrationDone then
		BFL:DebugPrint(BFL.L.MIGRATION_DEBUG_BNET .. " " .. #oldBnetUIDs)
	end
	if not wowMigrationDone then
		BFL:DebugPrint(BFL.L.MIGRATION_DEBUG_WOW .. " " .. #wowUIDsToMigrate)
	end

	-- Migrate Battle.net UIDs
	if not bnetMigrationDone then
		if #oldBnetUIDs > 0 then
			-- Remove old-style UIDs (they are now invalid and can't be migrated)
			for _, uid in ipairs(oldBnetUIDs) do
				DB:SetFriendGroups(uid, nil) -- Remove assignment
			end

			-- Inform user about the migration
			print("|cff00ff00BetterFriendlist:|r " .. BFL.L.MIGRATION_BNET_UPDATED)
			print("|cffff8800Note:|r " .. BFL.L.MIGRATION_BNET_REASSIGN)
			print("|cffaaaaaa" .. BFL.L.MIGRATION_BNET_REASON .. "|r")
		end
		-- Fix: Always set done flag even if 0 items found
		DB:Set("bnetUIDMigrationDone_v2", true)
	end

	-- Migrate WoW UIDs (add realm)
	if not wowMigrationDone then
		local markWoWDone = false

		if #wowUIDsToMigrate > 0 then
			local playerRealm = GetNormalizedRealmName()
			local migrated = 0

			if playerRealm and playerRealm ~= "" then
				for _, entry in ipairs(wowUIDsToMigrate) do
					local newUID = "wow_" .. entry.charName .. "-" .. playerRealm

					-- Copy groups to new UID
					DB:SetFriendGroups(newUID, entry.groups)

					-- Remove old UID
					DB:SetFriendGroups(entry.oldUID, nil)

					migrated = migrated + 1
				end

				print("|cff00ff00BetterFriendlist:|r " .. string.format(BFL.L.MIGRATION_WOW_RESULT, migrated))
				print("|cffaaaaaa" .. BFL.L.MIGRATION_WOW_FORMAT .. "|r")
				markWoWDone = true
			else
				print("|cffff8800BetterFriendlist:|r " .. BFL.L.MIGRATION_WOW_FAIL)
				-- Do not mark as done, try again later when realm is available
			end
		else
			-- Nothing to migrate, mark as done
			markWoWDone = true
		end

		if markWoWDone then
			DB:Set("wowUIDMigrationDone_v1", true)
		end
	end
end

-- Smart migration for WoW friends (v2) - Fixes missing realm in UIDs
function Groups:RunSmartMigration()
	local DB = BFL:GetModule("DB")
	if not DB then
		return
	end

	-- Check if already done
	if DB:Get("wowUIDMigrationDone_v2") then
		return
	end

	-- We need friend list data to be loaded
	local numFriends = (BFL.GetNumWoWFriends and BFL.GetNumWoWFriends() or 0)
	if not numFriends or numFriends == 0 then
		-- Try again later when list updates
		-- Prevent multiple registrations
		if not self.migrationListenerRegistered then
			BFL:RegisterEventCallback("FRIENDLIST_UPDATE", function()
				self:RunSmartMigration()
			end)
			self.migrationListenerRegistered = true
		end
		return
	end

	-- Perform migration
	local friendGroups = DB:GetFriendGroups()
	if not friendGroups then
		return
	end

	local changesMade = false

	-- Build a lookup of Name -> {FullNames...} from current friend list
	local nameLookup = {}
	for i = 1, numFriends do
		local info = BFL.GetWoWFriendInfoByIndex(i)
		if info and info.name then
			-- Normalize to ensure we have the full Name-Realm format
			local normalized = BFL:NormalizeWoWFriendName(info.name)
			if normalized then
				-- Extract short name (before hyphen)
				local shortName = normalized:match("^([^%-]+)")
				if shortName then
					nameLookup[shortName] = nameLookup[shortName] or {}
					table.insert(nameLookup[shortName], normalized)
				end
			end
		end
	end

	-- Check for old UIDs in database
	for uid, groups in pairs(friendGroups) do
		-- Look for "wow_Name" where Name has no hyphen
		if type(uid) == "string" and uid:match("^wow_[^%-]+$") then
			local shortName = uid:sub(5) -- Remove "wow_" prefix

			-- Check if we have EXACTLY ONE match in the friend list
			local matches = nameLookup[shortName]
			if matches and #matches == 1 then
				local correctFullName = matches[1] -- e.g. "Copium-Mal'Ganis"
				local newUID = "wow_" .. correctFullName

				-- Only migrate if the new UID is actually different (it should be, since old had no hyphen)
				if newUID ~= uid then
					-- Check if target already has groups (safety check)
					local existing = DB:GetFriendGroups(newUID)
					if not existing or #existing == 0 then
						print(
							"|cff00ff00BetterFriendlist:|r "
								.. string.format(BFL.L.MIGRATION_SMART_MIGRATING, shortName, correctFullName)
						)
						DB:SetFriendGroups(newUID, groups)
						DB:SetFriendGroups(uid, nil)
						changesMade = true
					end
				end
			end
		end
	end

	if changesMade then
		BFL:ForceRefreshFriendsList()
	end

	-- Mark as done
	DB:Set("wowUIDMigrationDone_v2", true)
end

-- Snapshot update: Explicitly set count/arrow colors to group color to disable implicit inheritance
function Groups:RunColorSnapshotMigration()
	local DB = BFL:GetModule("DB")
	if not DB then
		return
	end

	if DB:Get("colorSnapshotMigrationDone") then
		return
	end

	-- We iterate through valid groups in DB
	local modificationMade = false

	-- Snapshot Custom Groups
	local customGroups = DB:GetCustomGroups()
	if customGroups then
		for groupId, groupInfo in pairs(customGroups) do
			-- Check if specific colors are missing in DB
			local groupCountColors = DB:Get("groupCountColors") or {}
			local groupArrowColors = DB:Get("groupArrowColors") or {}

			-- If countColor is missing, snapshot current Group Color (or default Gold)
			if not groupCountColors[groupId] then
				-- Get current group color
				local groupColors = DB:Get("groupColors") or {}
				local currentColor = groupColors[groupId] or { r = 1.0, g = 0.82, b = 0.0, a = 1 }
				local currentAlpha = currentColor.a or 1

				-- Save snapshot
				groupCountColors[groupId] =
					{ r = currentColor.r, g = currentColor.g, b = currentColor.b, a = currentAlpha }
				DB:Set("groupCountColors", groupCountColors)
				modificationMade = true
			end

			-- If arrowColor is missing
			if not groupArrowColors[groupId] then
				local groupColors = DB:Get("groupColors") or {}
				local currentColor = groupColors[groupId] or { r = 1.0, g = 0.82, b = 0.0, a = 1 }
				local currentAlpha = currentColor.a or 1

				groupArrowColors[groupId] =
					{ r = currentColor.r, g = currentColor.g, b = currentColor.b, a = currentAlpha }
				DB:Set("groupArrowColors", groupArrowColors)
				modificationMade = true
			end
		end
	end

	-- Also handle Built-in Groups (Favorites, etc) if they have custom colors set
	-- (Actually built-in groups usually don't have custom colors unless overridden, which is handled via groupColors)
	local builtinIds = { "favorites", "ingame", "recentlyadded", "nogroup" }
	for _, groupId in ipairs(builtinIds) do
		local groupCountColors = DB:Get("groupCountColors") or {}
		local groupArrowColors = DB:Get("groupArrowColors") or {}

		if not groupCountColors[groupId] then
			local groupColors = DB:Get("groupColors") or {}
			-- Default for favorites/ingame is Gold, nogroup is Gray
			local defaultColor = (groupId == "nogroup") and { r = 0.5, g = 0.5, b = 0.5, a = 1 }
				or { r = 1.0, g = 0.82, b = 0.0, a = 1 }
			local currentColor = groupColors[groupId] or defaultColor

			groupCountColors[groupId] =
				{ r = currentColor.r, g = currentColor.g, b = currentColor.b, a = currentColor.a or 1 }
			DB:Set("groupCountColors", groupCountColors)
			modificationMade = true
		end

		if not groupArrowColors[groupId] then
			local groupColors = DB:Get("groupColors") or {}
			local defaultColor = (groupId == "nogroup") and { r = 0.5, g = 0.5, b = 0.5, a = 1 }
				or { r = 1.0, g = 0.82, b = 0.0, a = 1 }
			local currentColor = groupColors[groupId] or defaultColor

			groupArrowColors[groupId] =
				{ r = currentColor.r, g = currentColor.g, b = currentColor.b, a = currentColor.a or 1 }
			DB:Set("groupArrowColors", groupArrowColors)
			modificationMade = true
		end
	end

	if modificationMade then
		BFL:DebugPrint("Color Snapshot Migration applied.")
	end

	DB:Set("colorSnapshotMigrationDone", true)
end

function Groups:Initialize()
	-- Reset groups table to prevent stale data from persisting across re-initializations
	-- (e.g., after settings import where custom groups may have changed)
	wipe(self.groups)

	-- Localize built-in group names
	if BFL.L then
		builtinGroups.favorites.name = BFL.L.GROUP_FAVORITES
		builtinGroups.ingame.name = BFL.L.GROUP_INGAME
		builtinGroups.recentlyadded.name = BFL.L.GROUP_RECENTLY_ADDED or "Recently Added"
		builtinGroups.nogroup.name = BFL.L.GROUP_NO_GROUP
	end

	-- Copy built-in groups
	for id, data in pairs(builtinGroups) do
		self.groups[id] = CopyTable(data)
	end

	-- Load custom groups from database
	local DB = BFL:GetModule("DB")
	if not DB or not BetterFriendlistDB then
		-- DB not ready yet, skip initialization
		return
	end

	-- Load built-in group overrides
	local overrides = DB:Get("builtinGroupOverrides", {})
	for groupId, overrideData in pairs(overrides) do
		if self.groups[groupId] then
			if overrideData.name then
				self.groups[groupId].name = overrideData.name
			end
		end
	end

	-- Migrate old bnetAccountID-based friend assignments to battleTag-based
	-- Using pcall to prevent migration errors from blocking initialization
	local status, err = pcall(function()
		self:MigrateFriendAssignments()
	end)
	if not status then
		BFL:DebugPrint("Error in MigrateFriendAssignments: " .. tostring(err))
	end

	-- Run smart migration for WoW friends (v2)
	status, err = pcall(function()
		self:RunSmartMigration()
	end)
	if not status then
		BFL:DebugPrint("Error in RunSmartMigration: " .. tostring(err))
	end

	-- Run color snapshot migration (v3) - Disable implicit inheritance
	status, err = pcall(function()
		self:RunColorSnapshotMigration()
	end)
	if not status then
		BFL:DebugPrint("Error in RunColorSnapshotMigration: " .. tostring(err))
	end

	local customGroups = DB:GetCustomGroups()
	if customGroups then
		for groupId, groupInfo in pairs(customGroups) do
			self.groups[groupId] = {
				id = groupId,
				name = groupInfo.name,
				collapsed = groupInfo.collapsed or false,
				builtin = false,
				order = groupInfo.order or 50,
				color = { r = 1.0, g = 0.82, b = 0.0 }, -- Default gold
				icon = "Interface\\FriendsFrame\\UI-Toast-ChatInviteIcon",
			}
		end
	end

	-- Load collapsed states
	for groupId, groupData in pairs(self.groups) do
		local collapsed = DB:GetGroupState(groupId)
		if collapsed ~= nil then
			groupData.collapsed = collapsed
		end
	end

	-- Apply custom group colors from settings
	local groupColors = DB:Get("groupColors")
	if groupColors and type(groupColors) == "table" then
		for groupId, color in pairs(groupColors) do
			if self.groups[groupId] and color.r and color.g and color.b then
				self.groups[groupId].color = { r = color.r, g = color.g, b = color.b, a = color.a or 1 }
			end
		end
	end

	-- Apply custom group count colors
	local groupCountColors = DB:Get("groupCountColors")
	if groupCountColors and type(groupCountColors) == "table" then
		for groupId, color in pairs(groupCountColors) do
			if self.groups[groupId] and color.r and color.g and color.b then
				self.groups[groupId].countColor = { r = color.r, g = color.g, b = color.b, a = color.a or 1 }
			end
		end
	end

	-- Apply custom group arrow colors
	local groupArrowColors = DB:Get("groupArrowColors")
	if groupArrowColors and type(groupArrowColors) == "table" then
		for groupId, color in pairs(groupArrowColors) do
			if self.groups[groupId] and color.r and color.g and color.b then
				self.groups[groupId].arrowColor = { r = color.r, g = color.g, b = color.b, a = color.a or 1 }
			end
		end
	end

	-- Ensure all groups have countColor/arrowColor (fallback to group color)
	for groupId, groupData in pairs(self.groups) do
		if not groupData.countColor and groupData.color then
			groupData.countColor =
				{ r = groupData.color.r, g = groupData.color.g, b = groupData.color.b, a = groupData.color.a or 1 }
		end
		if not groupData.arrowColor and groupData.color then
			groupData.arrowColor =
				{ r = groupData.color.r, g = groupData.color.g, b = groupData.color.b, a = groupData.color.a or 1 }
		end
	end

	-- Apply saved group order from settings
	local savedOrder = DB:Get("groupOrder")
	if savedOrder and type(savedOrder) == "table" and #savedOrder > 0 then
		-- Apply order from saved settings
		for i, groupId in ipairs(savedOrder) do
			if self.groups[groupId] then
				self.groups[groupId].order = i
			end
		end
		-- Groups not in saved order get high order values
		for groupId, groupData in pairs(self.groups) do
			local found = false
			for _, savedId in ipairs(savedOrder) do
				if savedId == groupId then
					found = true
					break
				end
			end
			if not found then
				groupData.order = 1000 + #savedOrder
			end
		end
	end

	-- DEFENSIVE: Clean up ghost and built-in group entries in friendGroups.
	-- Built-in groups are derived dynamically and must never be persisted as assignments.
	if BetterFriendlistDB.friendGroups then
		local cleanedCount = 0
		for uid, groups in pairs(BetterFriendlistDB.friendGroups) do
			if type(groups) == "table" then
				for i = #groups, 1, -1 do
					local gid = groups[i]
					local group = type(gid) == "string" and self.groups[gid]
					-- A valid assignment must reference a loaded custom group.
					if not group or group.builtin then
						table.remove(groups, i)
						cleanedCount = cleanedCount + 1
					end
				end
				-- Clean up empty friend entries
				if #groups == 0 then
					BetterFriendlistDB.friendGroups[uid] = nil
				end
			else
				-- Invalid entry (not a table), remove it
				BetterFriendlistDB.friendGroups[uid] = nil
				cleanedCount = cleanedCount + 1
			end
		end
		if cleanedCount > 0 then
			-- BFL:DebugPrint("|cffff8800[Groups]|r Cleaned " .. cleanedCount .. " ghost group entries from friendGroups")
		end
	end
end

-- Get all groups
function Groups:GetAll()
	-- Check if DB is initialized before checking settings
	local DB = BFL:GetModule("DB")
	if not DB or not BetterFriendlistDB then
		-- DB not yet initialized, return all groups including favorites
		return self.groups
	end

	local showFavorites = DB:Get("showFavoritesGroup", true)
	local enableInGame = DB:Get("enableInGameGroup", false)
	local enableRecentlyAdded = DB:Get("enableRecentlyAddedGroup", false)

	-- Filter out hidden built-in groups
	if not showFavorites or not enableInGame or not enableRecentlyAdded then
		local filtered = {}
		for id, group in pairs(self.groups) do
			local keep = true
			if id == "favorites" and not showFavorites then
				keep = false
			end
			if id == "ingame" and not enableInGame then
				keep = false
			end
			if id == "recentlyadded" and not enableRecentlyAdded then
				keep = false
			end

			if keep then
				filtered[id] = group
			end
		end
		return filtered
	end

	return self.groups
end

-- Get a specific group
function Groups:Get(groupId)
	return self.groups[groupId]
end

-- Get group ID by name (for migration)
function Groups:GetGroupIdByName(groupName)
	for groupId, groupData in pairs(self.groups) do
		if groupData.name == groupName then
			return groupId
		end
	end
	return nil
end

-- Validate if a group name can be used
function Groups:ValidateGroupName(groupName, currentGroupId)
	if not groupName or groupName:gsub("%s+", "") == "" then
		return false, BFL.L.ERROR_GROUP_NAME_EMPTY or "Group name cannot be empty"
	end

	local lowerName = groupName:lower()

	-- Check for name collision across ALL groups
	for id, group in pairs(self.groups) do
		-- If renaming, ignore self
		if id ~= currentGroupId then
			if group.name and group.name:lower() == lowerName then
				return false, BFL.L.ERROR_GROUP_EXISTS or "Group already exists"
			end
		end
	end

	-- For creation (no currentGroupId), we also need to check if the generated ID is available
	-- because Create() uses deterministic IDs based on name.
	if not currentGroupId then
		-- Generate unique ID from name (same logic as Create)
		local potentialId = "custom_" .. groupName:gsub("%s+", "_"):lower()
		local existingGroup = self.groups[potentialId]

		if existingGroup then
			-- ID collision! Even if names are different (e.g. existing group was renamed away from this name)
			-- we can't create a new group with this name because the ID slot is taken.
			return false, BFL.L.ERROR_GROUP_EXISTS or "Group already exists"
		end
	end

	return true
end

-- Create a new custom group
function Groups:Create(groupName)
	if not groupName or groupName == "" then
		return false, BFL.L.ERROR_GROUP_NAME_EMPTY
	end

	-- Generate unique ID from name
	local groupId = "custom_" .. groupName:gsub("%s+", "_"):lower()

	-- Check if group already exists
	if self.groups[groupId] then
		return false, BFL.L.ERROR_GROUP_EXISTS
	end

	-- Find next order value (place custom groups between Favorites and No Group)
	local maxOrder = 1
	for id, groupData in pairs(self.groups) do
		-- Count all groups EXCEPT "nogroup" so we append after the last "content" group
		-- This fixes the bug where "ingame" group was ignored in the calculation
		if id ~= "nogroup" then
			maxOrder = math.max(maxOrder, groupData.order or 1)
		end
	end

	-- Create group
	self.groups[groupId] = {
		id = groupId,
		name = groupName,
		collapsed = false,
		builtin = false,
		order = maxOrder + 1,
		color = { r = 1.0, g = 0.82, b = 0.0 }, -- Default gold for all groups
		icon = "Interface\\FriendsFrame\\UI-Toast-ChatInviteIcon",
	}

	-- Save to database
	local DB = BFL:GetModule("DB")
	DB:SaveCustomGroup(groupId, {
		name = groupName,
		collapsed = false,
		order = maxOrder + 1,
	})

	-- Initialize color snapshot (Default Gold)
	local defaultColor = { r = 1.0, g = 0.82, b = 0.0, a = 1 }

	local groupCountColors = DB:Get("groupCountColors") or {}
	groupCountColors[groupId] = { r = defaultColor.r, g = defaultColor.g, b = defaultColor.b, a = defaultColor.a }
	DB:Set("groupCountColors", groupCountColors)

	local groupArrowColors = DB:Get("groupArrowColors") or {}
	groupArrowColors[groupId] = { r = defaultColor.r, g = defaultColor.g, b = defaultColor.b, a = defaultColor.a }
	DB:Set("groupArrowColors", groupArrowColors)

	self.groups[groupId].countColor = { r = defaultColor.r, g = defaultColor.g, b = defaultColor.b, a = defaultColor.a }
	self.groups[groupId].arrowColor = { r = defaultColor.r, g = defaultColor.g, b = defaultColor.b, a = defaultColor.a }

	-- Refresh Settings UI if open
	local Settings = BFL:GetModule("Settings")
	if Settings and BetterFriendlistSettingsFrame and BetterFriendlistSettingsFrame:IsShown() then
		Settings:RefreshGroupList()
	end

	return true, groupId
end

-- Create a new custom group with specific order (for migration)
function Groups:CreateWithOrder(groupName, orderValue)
	if not groupName or groupName == "" then
		return false, BFL.L.ERROR_GROUP_NAME_EMPTY
	end

	-- Generate unique ID from name
	local groupId = "custom_" .. groupName:gsub("%s+", "_"):lower()

	-- Check if group already exists
	if self.groups[groupId] then
		return false, BFL.L.ERROR_GROUP_EXISTS
	end

	-- Create group with specified order
	self.groups[groupId] = {
		id = groupId,
		name = groupName,
		collapsed = false,
		builtin = false,
		order = orderValue,
		color = { r = 1.0, g = 0.82, b = 0.0 }, -- Default gold for all groups
		icon = "Interface\\FriendsFrame\\UI-Toast-ChatInviteIcon",
	}

	-- Save to database
	local DB = BFL:GetModule("DB")
	DB:SaveCustomGroup(groupId, {
		name = groupName,
		collapsed = false,
		order = orderValue,
	})

	-- Initialize color snapshot (Default Gold)
	local defaultColor = { r = 1.0, g = 0.82, b = 0.0, a = 1 }

	local groupCountColors = DB:Get("groupCountColors") or {}
	groupCountColors[groupId] = { r = defaultColor.r, g = defaultColor.g, b = defaultColor.b, a = defaultColor.a }
	DB:Set("groupCountColors", groupCountColors)

	local groupArrowColors = DB:Get("groupArrowColors") or {}
	groupArrowColors[groupId] = { r = defaultColor.r, g = defaultColor.g, b = defaultColor.b, a = defaultColor.a }
	DB:Set("groupArrowColors", groupArrowColors)

	self.groups[groupId].countColor = { r = defaultColor.r, g = defaultColor.g, b = defaultColor.b, a = defaultColor.a }
	self.groups[groupId].arrowColor = { r = defaultColor.r, g = defaultColor.g, b = defaultColor.b, a = defaultColor.a }

	-- Refresh Settings UI if open
	local Settings = BFL:GetModule("Settings")
	if Settings and BetterFriendlistSettingsFrame and BetterFriendlistSettingsFrame:IsShown() then
		Settings:RefreshGroupList()
	end

	return true, groupId
end

-- Rename a custom group
function Groups:Rename(groupId, newName)
	if not groupId or not newName or newName == "" then
		return false, BFL.L.ERROR_INVALID_GROUP_NAME
	end

	-- Verify name validity using our centralized validator
	local isValid, errorMsg = self:ValidateGroupName(newName, groupId)
	if not isValid then
		return false, errorMsg
	end

	local group = self.groups[groupId]
	if not group then
		return false, BFL.L.ERROR_GROUP_NOT_EXIST
	end

	-- Update in memory
	group.name = newName

	local DB = BFL:GetModule("DB")

	if group.builtin then
		-- Save override for built-in group
		local overrides = DB:Get("builtinGroupOverrides", {})
		overrides[groupId] = overrides[groupId] or {}
		overrides[groupId].name = newName
		DB:Set("builtinGroupOverrides", overrides)
	else
		-- Update custom group in database
		local groupInfo = DB:GetCustomGroup(groupId)
		if groupInfo then
			groupInfo.name = newName
			DB:SaveCustomGroup(groupId, groupInfo)
		end
	end

	-- Refresh Settings UI if open
	local Settings = BFL:GetModule("Settings")
	if Settings and BetterFriendlistSettingsFrame and BetterFriendlistSettingsFrame:IsShown() then
		Settings:RefreshGroupList()
	end

	return true
end

-- Set group color (Fix #34: Added alpha support)
function Groups:SetColor(groupId, r, g, b, a)
	if not groupId or not r or not g or not b then
		return false, BFL.L.ERROR_INVALID_PARAMS
	end

	local group = self.groups[groupId]
	if not group then
		return false, BFL.L.ERROR_GROUP_NOT_EXIST
	end

	-- Fix #34: Default alpha to 1 if not provided
	local alpha = a or 1

	-- Update in memory
	group.color = { r = r, g = g, b = b, a = alpha }

	local DB = BFL:GetModule("DB")

	-- Save to database (same structure for built-in and custom)
	local groupColors = DB:Get("groupColors", {})
	groupColors[groupId] = { r = r, g = g, b = b, a = alpha }
	DB:Set("groupColors", groupColors)

	-- Refresh Settings UI if open
	local Settings = BFL:GetModule("Settings")
	if Settings and BetterFriendlistSettingsFrame and BetterFriendlistSettingsFrame:IsShown() then
		Settings:RefreshGroupList()
	end

	return true
end

-- Delete a custom group
function Groups:Delete(groupId)
	if not groupId then
		return false, BFL.L.ERROR_INVALID_GROUP_ID
	end

	local group = self.groups[groupId]
	if not group then
		return false, BFL.L.ERROR_GROUP_NOT_EXIST
	end

	if group.builtin then
		return false, BFL.L.ERROR_CANNOT_DELETE_BUILTIN
	end

	-- Remove from memory
	self.groups[groupId] = nil

	-- Remove from database
	local DB = BFL:GetModule("DB")
	DB:DeleteCustomGroup(groupId)

	-- Refresh Settings UI if open
	local Settings = BFL:GetModule("Settings")
	if Settings and BetterFriendlistSettingsFrame and BetterFriendlistSettingsFrame:IsShown() then
		Settings:RefreshGroupList()
	end

	-- Remove all friend assignments to this group
	for _, friendUID in ipairs(DB:GetAllFriendUIDs()) do
		DB:RemoveFriendFromGroup(friendUID, groupId)
	end

	-- Force refresh friends list so removed friends appear in "No Group" immediately
	BFL:ForceRefreshFriendsList()

	return true
end

-- Toggle group collapsed state
function Groups:Toggle(groupId, suppressUpdate)
	local group = self.groups[groupId]
	if not group then
		return false
	end

	group.collapsed = not group.collapsed

	-- Save to database
	local DB = BFL:GetModule("DB")
	DB:SetGroupState(groupId, group.collapsed)

	-- Force refresh of the friends list to update UI immediately
	if not suppressUpdate then
		BFL:ForceRefreshFriendsList()
	end

	return true
end

-- Set group collapsed state (for Collapse/Expand All)
function Groups:SetCollapsed(groupId, collapsed, suppressUpdate)
	local group = self.groups[groupId]
	if not group then
		return false
	end

	group.collapsed = collapsed

	-- Save to database
	local DB = BFL:GetModule("DB")
	DB:SetGroupState(groupId, group.collapsed)

	-- Force refresh of the friends list to update UI immediately
	if not suppressUpdate then
		BFL:ForceRefreshFriendsList()
	end

	return true
end

-- Check if friend is in group
function Groups:IsFriendInGroup(friendUID, groupId)
	local DB = BFL:GetModule("DB")
	return DB:IsFriendInGroup(friendUID, groupId)
end

-- Toggle friend in group (add if not present, remove if present)
function Groups:ToggleFriendInGroup(friendUID, groupId)
	if not friendUID or not groupId then
		return false
	end

	-- Don't allow adding to builtin groups (except they're managed automatically)
	if self.groups[groupId] and self.groups[groupId].builtin then
		return false
	end

	local DB = BFL:GetModule("DB")

	if DB:IsFriendInGroup(friendUID, groupId) then
		-- Remove from group
		return DB:RemoveFriendFromGroup(friendUID, groupId)
	else
		-- Add to group
		return DB:AddFriendToGroup(friendUID, groupId)
	end
end

-- Remove friend from group
function Groups:RemoveFriendFromGroup(friendUID, groupId)
	local DB = BFL:GetModule("DB")
	return DB:RemoveFriendFromGroup(friendUID, groupId)
end

-- Get all groups a friend is in
function Groups:GetFriendGroups(friendUID)
	local DB = BFL:GetModule("DB")
	return DB:GetFriendGroups(friendUID)
end

-- Get count of friends in a group
function Groups:GetMemberCount(groupId, friendsList)
	local count = 0
	local DB = BFL:GetModule("DB")

	for _, friend in ipairs(friendsList or {}) do
		local friendUID = self:GetFriendUID(friend)
		if friendUID then
			-- Check for built-in groups
			if groupId == "favorites" and friend.type == "bnet" and friend.isFavorite then
				count = count + 1
			elseif groupId == "nogroup" then
				-- Check if friend is in any custom group
				local inAnyGroup = false
				if friend.type == "bnet" and friend.isFavorite then
					inAnyGroup = true
				end
				if not inAnyGroup and DB:GetFriendGroups(friendUID) then
					for _, gid in ipairs(DB:GetFriendGroups(friendUID)) do
						if self.groups[gid] and not self.groups[gid].builtin then
							inAnyGroup = true
							break
						end
					end
				end
				if not inAnyGroup then
					count = count + 1
				end
			else
				-- Check for custom groups
				if DB:IsFriendInGroup(friendUID, groupId) then
					count = count + 1
				end
			end
		end
	end

	return count
end

-- Helper function to get friend unique ID
function Groups:GetFriendUID(friend)
	if not friend then
		return nil
	end
	if friend.type == "bnet" then
		-- Use battleTag as persistent identifier (bnetAccountID is temporary per session)
		if friend.battleTag and friend.battleTag ~= "" then
			return "bnet_" .. friend.battleTag
		else
			-- Fallback to bnetAccountID only if battleTag is unavailable (should never happen)
			return "bnet_" .. tostring(friend.bnetAccountID or "unknown")
		end
	else
		return "wow_" .. (friend.name or "")
	end
end

-- Expose GetFriendUID globally for backward compatibility
_G.GetFriendUID = function(friend)
	return Groups:GetFriendUID(friend)
end

-- Get sorted list of group IDs based on their configured order
-- Excludes "nogroup" by default since it's displayed separately at the end
function Groups:GetSortedGroupIds(includeNoGroup)
	local allGroups = self:GetAll()
	local sortedIds = {}

	for groupId, groupData in pairs(allGroups) do
		if includeNoGroup or groupId ~= "nogroup" then
			table.insert(sortedIds, {
				id = groupId,
				order = groupData.order or 999,
			})
		end
	end

	-- Sort by order value
	table.sort(sortedIds, function(a, b)
		return a.order < b.order
	end)

	-- Extract just the IDs in sorted order
	local result = {}
	for _, entry in ipairs(sortedIds) do
		table.insert(result, entry.id)
	end

	return result
end
