package groups import ( "fmt" "maps" "strings" "sync" "time" ) // MasterList manages groups with optimized lookups for: // - Fast ID-based lookups (O(1)) // - Fast member-based lookups (indexed) // - Fast zone-based filtering (indexed) // - Fast size-based filtering (indexed) // - Raid group management and lookups // - Leader-based searching // - Activity tracking and cleanup type MasterList struct { // Core storage groups map[int32]*Group // ID -> Group mutex sync.RWMutex // Indices for O(1) lookups byMember map[string]*Group // Member name -> group containing that member byLeader map[string]*Group // Leader name -> group byZone map[int32][]*Group // Zone ID -> groups with members in that zone bySize map[int32][]*Group // Size -> groups of that size activeGroups map[int32]*Group // Active (non-disbanded) groups raidGroups map[int32]*Group // Groups that are part of raids soloGroups map[int32]*Group // Single-member groups fullGroups map[int32]*Group // Full groups (size = MAX_GROUP_SIZE) // Activity tracking byLastActivity map[time.Time][]*Group // Last activity time -> groups // Cached metadata and slices totalMembers int32 // Total active members across all groups zones []int32 // Unique zones with group members sizes []int32 // Unique group sizes zoneStats map[int32]int // Zone ID -> group count sizeStats map[int32]int // Size -> group count allGroupsSlice []*Group // Cached slice of all groups activeGroupsSlice []*Group // Cached slice of active groups metaStale bool // Whether metadata cache needs refresh } // NewMasterList creates a new group master list func NewMasterList() *MasterList { return &MasterList{ groups: make(map[int32]*Group), byMember: make(map[string]*Group), byLeader: make(map[string]*Group), byZone: make(map[int32][]*Group), bySize: make(map[int32][]*Group), activeGroups: make(map[int32]*Group), raidGroups: make(map[int32]*Group), soloGroups: make(map[int32]*Group), fullGroups: make(map[int32]*Group), byLastActivity: make(map[time.Time][]*Group), zoneStats: make(map[int32]int), sizeStats: make(map[int32]int), allGroupsSlice: make([]*Group, 0), activeGroupsSlice: make([]*Group, 0), metaStale: true, } } // refreshMetaCache updates the cached metadata func (ml *MasterList) refreshMetaCache() { if !ml.metaStale { return } // Clear and rebuild zone and size stats ml.zoneStats = make(map[int32]int) ml.sizeStats = make(map[int32]int) zoneSet := make(map[int32]struct{}) sizeSet := make(map[int32]struct{}) ml.totalMembers = 0 // Collect unique values and stats for _, group := range ml.activeGroups { size := group.GetSize() ml.sizeStats[size]++ sizeSet[size] = struct{}{} ml.totalMembers += size // Collect zones from group members members := group.GetMembers() zoneMap := make(map[int32]struct{}) for _, member := range members { if member.ZoneID > 0 { zoneMap[member.ZoneID] = struct{}{} } } for zoneID := range zoneMap { ml.zoneStats[zoneID]++ zoneSet[zoneID] = struct{}{} } } // Clear and rebuild cached slices ml.zones = ml.zones[:0] for zoneID := range zoneSet { ml.zones = append(ml.zones, zoneID) } ml.sizes = ml.sizes[:0] for size := range sizeSet { ml.sizes = append(ml.sizes, size) } // Rebuild all groups slice ml.allGroupsSlice = ml.allGroupsSlice[:0] if cap(ml.allGroupsSlice) < len(ml.groups) { ml.allGroupsSlice = make([]*Group, 0, len(ml.groups)) } for _, group := range ml.groups { ml.allGroupsSlice = append(ml.allGroupsSlice, group) } // Rebuild active groups slice ml.activeGroupsSlice = ml.activeGroupsSlice[:0] if cap(ml.activeGroupsSlice) < len(ml.activeGroups) { ml.activeGroupsSlice = make([]*Group, 0, len(ml.activeGroups)) } for _, group := range ml.activeGroups { ml.activeGroupsSlice = append(ml.activeGroupsSlice, group) } ml.metaStale = false } // updateGroupIndices updates all indices for a group func (ml *MasterList) updateGroupIndices(group *Group, add bool) { groupID := group.GetID() size := group.GetSize() leaderName := group.GetLeaderName() isRaid := group.IsGroupRaid() isDisbanded := group.IsDisbanded() members := group.GetMembers() if add { // Add to size index ml.bySize[size] = append(ml.bySize[size], group) // Add to leader index if leaderName != "" { ml.byLeader[strings.ToLower(leaderName)] = group } // Add to member index for _, member := range members { if member.Name != "" { ml.byMember[strings.ToLower(member.Name)] = group } } // Add to zone index zoneMap := make(map[int32]struct{}) for _, member := range members { if member.ZoneID > 0 { zoneMap[member.ZoneID] = struct{}{} } } for zoneID := range zoneMap { ml.byZone[zoneID] = append(ml.byZone[zoneID], group) } // Add to specialized indices if !isDisbanded { ml.activeGroups[groupID] = group } if isRaid { ml.raidGroups[groupID] = group } if size == 1 { ml.soloGroups[groupID] = group } if size == MAX_GROUP_SIZE { ml.fullGroups[groupID] = group } // Add to activity index activity := group.GetLastActivity() ml.byLastActivity[activity] = append(ml.byLastActivity[activity], group) } else { // Remove from size index sizeGroups := ml.bySize[size] for i, g := range sizeGroups { if g.GetID() == groupID { ml.bySize[size] = append(sizeGroups[:i], sizeGroups[i+1:]...) break } } // Remove from leader index if leaderName != "" { delete(ml.byLeader, strings.ToLower(leaderName)) } // Remove from member index for _, member := range members { if member.Name != "" { delete(ml.byMember, strings.ToLower(member.Name)) } } // Remove from zone index zoneMap := make(map[int32]struct{}) for _, member := range members { if member.ZoneID > 0 { zoneMap[member.ZoneID] = struct{}{} } } for zoneID := range zoneMap { zoneGroups := ml.byZone[zoneID] for i, g := range zoneGroups { if g.GetID() == groupID { ml.byZone[zoneID] = append(zoneGroups[:i], zoneGroups[i+1:]...) break } } } // Remove from specialized indices delete(ml.activeGroups, groupID) delete(ml.raidGroups, groupID) delete(ml.soloGroups, groupID) delete(ml.fullGroups, groupID) // Remove from activity index activity := group.GetLastActivity() activityGroups := ml.byLastActivity[activity] for i, g := range activityGroups { if g.GetID() == groupID { ml.byLastActivity[activity] = append(activityGroups[:i], activityGroups[i+1:]...) break } } } } // AddGroup adds a group with full indexing func (ml *MasterList) AddGroup(group *Group) bool { if group == nil { return false } ml.mutex.Lock() defer ml.mutex.Unlock() // Check if exists if _, exists := ml.groups[group.GetID()]; exists { return false } // Add to core storage ml.groups[group.GetID()] = group // Update all indices ml.updateGroupIndices(group, true) // Invalidate metadata cache ml.metaStale = true return true } // GetGroup retrieves by ID (O(1)) func (ml *MasterList) GetGroup(groupID int32) *Group { ml.mutex.RLock() defer ml.mutex.RUnlock() return ml.groups[groupID] } // GetGroupSafe retrieves a group by ID with existence check func (ml *MasterList) GetGroupSafe(groupID int32) (*Group, bool) { ml.mutex.RLock() defer ml.mutex.RUnlock() group, exists := ml.groups[groupID] return group, exists } // HasGroup checks if a group exists by ID func (ml *MasterList) HasGroup(groupID int32) bool { ml.mutex.RLock() defer ml.mutex.RUnlock() _, exists := ml.groups[groupID] return exists } // RemoveGroup removes a group and updates all indices func (ml *MasterList) RemoveGroup(groupID int32) bool { ml.mutex.Lock() defer ml.mutex.Unlock() group, exists := ml.groups[groupID] if !exists { return false } // Remove from core storage delete(ml.groups, groupID) // Update all indices ml.updateGroupIndices(group, false) // Invalidate metadata cache ml.metaStale = true return true } // GetAllGroups returns all groups as a slice func (ml *MasterList) GetAllGroups() []*Group { ml.mutex.Lock() // Need write lock to potentially update cache defer ml.mutex.Unlock() ml.refreshMetaCache() // Return a copy to prevent external modification result := make([]*Group, len(ml.allGroupsSlice)) copy(result, ml.allGroupsSlice) return result } // GetAllGroupsMap returns a copy of all groups map func (ml *MasterList) GetAllGroupsMap() map[int32]*Group { ml.mutex.RLock() defer ml.mutex.RUnlock() // Return a copy to prevent external modification result := make(map[int32]*Group, len(ml.groups)) maps.Copy(result, ml.groups) return result } // GetGroupCount returns the total number of groups func (ml *MasterList) GetGroupCount() int32 { ml.mutex.RLock() defer ml.mutex.RUnlock() return int32(len(ml.groups)) } // Size returns the total number of groups func (ml *MasterList) Size() int { ml.mutex.RLock() defer ml.mutex.RUnlock() return len(ml.groups) } // IsEmpty returns true if the master list is empty func (ml *MasterList) IsEmpty() bool { ml.mutex.RLock() defer ml.mutex.RUnlock() return len(ml.groups) == 0 } // Clear removes all groups from the list func (ml *MasterList) Clear() { ml.mutex.Lock() defer ml.mutex.Unlock() // Clear all maps ml.groups = make(map[int32]*Group) ml.byMember = make(map[string]*Group) ml.byLeader = make(map[string]*Group) ml.byZone = make(map[int32][]*Group) ml.bySize = make(map[int32][]*Group) ml.activeGroups = make(map[int32]*Group) ml.raidGroups = make(map[int32]*Group) ml.soloGroups = make(map[int32]*Group) ml.fullGroups = make(map[int32]*Group) ml.byLastActivity = make(map[time.Time][]*Group) // Clear cached metadata ml.zones = ml.zones[:0] ml.sizes = ml.sizes[:0] ml.allGroupsSlice = ml.allGroupsSlice[:0] ml.activeGroupsSlice = ml.activeGroupsSlice[:0] ml.zoneStats = make(map[int32]int) ml.sizeStats = make(map[int32]int) ml.totalMembers = 0 ml.metaStale = true } // GetGroupsByFilter returns groups matching the filter function func (ml *MasterList) GetGroupsByFilter(filter func(*Group) bool) []*Group { ml.mutex.RLock() defer ml.mutex.RUnlock() var result []*Group for _, group := range ml.groups { if filter(group) { result = append(result, group) } } return result } // GetGroupByMember returns the group containing the specified member (O(1)) func (ml *MasterList) GetGroupByMember(memberName string) *Group { ml.mutex.RLock() defer ml.mutex.RUnlock() return ml.byMember[strings.ToLower(memberName)] } // GetGroupByLeader returns the group led by the specified leader (O(1)) func (ml *MasterList) GetGroupByLeader(leaderName string) *Group { ml.mutex.RLock() defer ml.mutex.RUnlock() return ml.byLeader[strings.ToLower(leaderName)] } // GetActiveGroups returns all non-disbanded groups (O(1)) func (ml *MasterList) GetActiveGroups() []*Group { ml.mutex.Lock() // Need write lock to potentially update cache defer ml.mutex.Unlock() ml.refreshMetaCache() // Return a copy to prevent external modification result := make([]*Group, len(ml.activeGroupsSlice)) copy(result, ml.activeGroupsSlice) return result } // GetGroupsByZone returns groups with members in the specified zone (O(1)) func (ml *MasterList) GetGroupsByZone(zoneID int32) []*Group { ml.mutex.RLock() defer ml.mutex.RUnlock() return ml.byZone[zoneID] } // GetGroupsBySize returns groups of the specified size (O(1)) func (ml *MasterList) GetGroupsBySize(size int32) []*Group { ml.mutex.RLock() defer ml.mutex.RUnlock() return ml.bySize[size] } // GetRaidGroups returns all groups that are part of raids (O(1)) func (ml *MasterList) GetRaidGroups() []*Group { ml.mutex.RLock() defer ml.mutex.RUnlock() result := make([]*Group, 0, len(ml.raidGroups)) for _, group := range ml.raidGroups { result = append(result, group) } return result } // GetSoloGroups returns all groups with only one member (O(1)) func (ml *MasterList) GetSoloGroups() []*Group { ml.mutex.RLock() defer ml.mutex.RUnlock() result := make([]*Group, 0, len(ml.soloGroups)) for _, group := range ml.soloGroups { result = append(result, group) } return result } // GetFullGroups returns all groups at maximum capacity (O(1)) func (ml *MasterList) GetFullGroups() []*Group { ml.mutex.RLock() defer ml.mutex.RUnlock() result := make([]*Group, 0, len(ml.fullGroups)) for _, group := range ml.fullGroups { result = append(result, group) } return result } // GetGroupsByLeader returns groups led by entities with the specified name func (ml *MasterList) GetGroupsByLeader(leaderName string) []*Group { group := ml.GetGroupByLeader(leaderName) if group != nil { return []*Group{group} } return []*Group{} } // GetGroupsByMember returns groups containing a member with the specified name func (ml *MasterList) GetGroupsByMember(memberName string) []*Group { group := ml.GetGroupByMember(memberName) if group != nil { return []*Group{group} } return []*Group{} } // GetZones returns all unique zones with group members using cached results func (ml *MasterList) GetZones() []int32 { ml.mutex.Lock() // Need write lock to potentially update cache defer ml.mutex.Unlock() ml.refreshMetaCache() // Return a copy to prevent external modification result := make([]int32, len(ml.zones)) copy(result, ml.zones) return result } // GetSizes returns all unique group sizes using cached results func (ml *MasterList) GetSizes() []int32 { ml.mutex.Lock() // Need write lock to potentially update cache defer ml.mutex.Unlock() ml.refreshMetaCache() // Return a copy to prevent external modification result := make([]int32, len(ml.sizes)) copy(result, ml.sizes) return result } // GetTotalMembers returns the total number of active members across all groups func (ml *MasterList) GetTotalMembers() int32 { ml.mutex.Lock() // Need write lock to potentially update cache defer ml.mutex.Unlock() ml.refreshMetaCache() return ml.totalMembers } // GetGroupStatistics returns statistics about the groups in the master list using cached data func (ml *MasterList) GetGroupStatistics() *GroupMasterListStats { ml.mutex.Lock() // Need write lock to potentially update cache defer ml.mutex.Unlock() ml.refreshMetaCache() var totalRaidMembers int32 for _, group := range ml.raidGroups { totalRaidMembers += group.GetSize() } var averageGroupSize float64 if len(ml.activeGroups) > 0 { averageGroupSize = float64(ml.totalMembers) / float64(len(ml.activeGroups)) } return &GroupMasterListStats{ TotalGroups: int32(len(ml.groups)), ActiveGroups: int32(len(ml.activeGroups)), RaidGroups: int32(len(ml.raidGroups)), TotalMembers: ml.totalMembers, TotalRaidMembers: totalRaidMembers, AverageGroupSize: averageGroupSize, SoloGroups: int32(len(ml.soloGroups)), FullGroups: int32(len(ml.fullGroups)), } } // GroupMasterListStats holds statistics about the groups master list type GroupMasterListStats struct { TotalGroups int32 `json:"total_groups"` ActiveGroups int32 `json:"active_groups"` RaidGroups int32 `json:"raid_groups"` TotalMembers int32 `json:"total_members"` TotalRaidMembers int32 `json:"total_raid_members"` AverageGroupSize float64 `json:"average_group_size"` SoloGroups int32 `json:"solo_groups"` FullGroups int32 `json:"full_groups"` } // RefreshGroupIndices refreshes indices for a group (used when group state changes) func (ml *MasterList) RefreshGroupIndices(group *Group) { ml.mutex.Lock() defer ml.mutex.Unlock() // Remove from old indices ml.updateGroupIndices(group, false) // Add to new indices ml.updateGroupIndices(group, true) // Invalidate metadata cache ml.metaStale = true } // UpdateGroup updates an existing group and refreshes indices func (ml *MasterList) UpdateGroup(group *Group) error { if group == nil { return fmt.Errorf("group cannot be nil") } ml.mutex.Lock() defer ml.mutex.Unlock() // Check if exists old, exists := ml.groups[group.GetID()] if !exists { return fmt.Errorf("group %d not found", group.GetID()) } // Remove old group from indices (but not core storage yet) ml.updateGroupIndices(old, false) // Update core storage ml.groups[group.GetID()] = group // Add new group to indices ml.updateGroupIndices(group, true) // Invalidate metadata cache ml.metaStale = true return nil } // ForEach executes a function for each group func (ml *MasterList) ForEach(fn func(int32, *Group)) { ml.mutex.RLock() defer ml.mutex.RUnlock() for id, group := range ml.groups { fn(id, group) } } // Cleanup removes disbanded groups from the master list func (ml *MasterList) Cleanup() int32 { ml.mutex.Lock() defer ml.mutex.Unlock() removed := int32(0) for id, group := range ml.groups { if group.IsDisbanded() { // Remove from core storage delete(ml.groups, id) // Remove from indices (group is already disbanded, so activeGroups won't include it) ml.updateGroupIndices(group, false) removed++ } } if removed > 0 { // Invalidate metadata cache ml.metaStale = true } return removed } // ValidateAll validates all groups in the master list func (ml *MasterList) ValidateAll() []error { ml.mutex.RLock() defer ml.mutex.RUnlock() var errors []error for id, group := range ml.groups { if group == nil { errors = append(errors, fmt.Errorf("group ID %d is nil", id)) continue } // Check for basic validity if group.GetID() != id { errors = append(errors, fmt.Errorf("group ID mismatch: map key %d != group ID %d", id, group.GetID())) } if group.GetID() <= 0 { errors = append(errors, fmt.Errorf("group %d has invalid ID", group.GetID())) } if group.GetSize() == 0 && !group.IsDisbanded() { errors = append(errors, fmt.Errorf("group %d is empty but not disbanded", group.GetID())) } if group.GetSize() > MAX_GROUP_SIZE { errors = append(errors, fmt.Errorf("group %d exceeds maximum size (%d > %d)", group.GetID(), group.GetSize(), MAX_GROUP_SIZE)) } // Check for leader members := group.GetMembers() hasLeader := false leaderCount := 0 for _, member := range members { if member.Leader { hasLeader = true leaderCount++ } } if !hasLeader && !group.IsDisbanded() { errors = append(errors, fmt.Errorf("group %d has no leader", group.GetID())) } if leaderCount > 1 { errors = append(errors, fmt.Errorf("group %d has multiple leaders (%d)", group.GetID(), leaderCount)) } // Validate index consistency if !group.IsDisbanded() { if _, exists := ml.activeGroups[id]; !exists { errors = append(errors, fmt.Errorf("active group %d not found in activeGroups index", id)) } } if group.IsGroupRaid() { if _, exists := ml.raidGroups[id]; !exists { errors = append(errors, fmt.Errorf("raid group %d not found in raidGroups index", id)) } } if group.GetSize() == 1 { if _, exists := ml.soloGroups[id]; !exists { errors = append(errors, fmt.Errorf("solo group %d not found in soloGroups index", id)) } } if group.GetSize() == MAX_GROUP_SIZE { if _, exists := ml.fullGroups[id]; !exists { errors = append(errors, fmt.Errorf("full group %d not found in fullGroups index", id)) } } } return errors } // IsValid returns true if all groups are valid func (ml *MasterList) IsValid() bool { errors := ml.ValidateAll() return len(errors) == 0 }