package roster import ( "database/sql" "fmt" "log" "sort" "strconv" "strings" "github.com/bwmarrin/discordgo" "ffxiv-bot/db" "ffxiv-bot/tomestone" ) const playersPerPage = 30 type Manager struct { DB *sql.DB ChannelID string Session *discordgo.Session ActivityIDToLabel map[string]string } func (m *Manager) Refresh() { chars, err := db.GetAllCharacters(m.DB) if err != nil { log.Printf("[Roster] Error fetching characters: %v", err) return } userChars := make(map[string][]db.Character) for _, c := range chars { userChars[c.DiscordID] = append(userChars[c.DiscordID], c) } discordIDs := make([]string, 0, len(userChars)) for id := range userChars { discordIDs = append(discordIDs, id) } sort.Strings(discordIDs) allProgression, err := db.GetAllProgression(m.DB) if err != nil { log.Printf("[Roster] Error fetching progression: %v", err) allProgression = nil } aggProgression := aggregateProgression(allProgression, userChars) pages := chunkPlayers(discordIDs, userChars, playersPerPage) totalPages := len(pages) existing, err := db.GetAllRosterMessages(m.DB) if err != nil { log.Printf("[Roster] Error fetching existing roster messages: %v", err) return } existingPages := make(map[int]string) for _, rm := range existing { existingPages[rm.Page] = rm.DiscordMessageID } encounterOrder := sortedEncounterKeys(m.ActivityIDToLabel) for pageIdx, page := range pages { pageNum := pageIdx + 1 content := formatPage(page, userChars, aggProgression, encounterOrder, m.ActivityIDToLabel, pageNum, totalPages) msgID, exists := existingPages[pageNum] if exists { if _, err := m.Session.ChannelMessageEdit(m.ChannelID, msgID, content); err != nil { log.Printf("[Roster] Error updating message for page %d: %v", pageNum, err) msgID = "" } } if msgID == "" { msg, err := m.Session.ChannelMessageSend(m.ChannelID, content) if err != nil { log.Printf("[Roster] Error sending message for page %d: %v", pageNum, err) continue } msgID = msg.ID } if err := db.SaveRosterMessage(m.DB, pageNum, msgID); err != nil { log.Printf("[Roster] Error saving roster message for page %d: %v", pageNum, err) } existingPages[pageNum] = msgID } for pageToDelete := len(pages) + 1; ; pageToDelete++ { msgID, exists := existingPages[pageToDelete] if !exists { break } if err := m.Session.ChannelMessageDelete(m.ChannelID, msgID); err != nil { log.Printf("[Roster] Error deleting extra message for page %d: %v", pageToDelete, err) } if err := db.DeleteRosterMessage(m.DB, pageToDelete); err != nil { log.Printf("[Roster] Error deleting roster message record for page %d: %v", pageToDelete, err) } delete(existingPages, pageToDelete) } if len(pages) == 0 { for _, rm := range existing { if err := m.Session.ChannelMessageDelete(m.ChannelID, rm.DiscordMessageID); err != nil { log.Printf("[Roster] Error deleting orphan roster message: %v", err) } if err := db.DeleteRosterMessage(m.DB, rm.Page); err != nil { log.Printf("[Roster] Error deleting orphan roster message record: %v", err) } } } log.Printf("[Roster] Refreshed roster: %d players across %d page(s)", len(discordIDs), totalPages) } func sortedEncounterKeys(activityIDToLabel map[string]string) []int { var keys []int for k := range activityIDToLabel { v, err := strconv.Atoi(k) if err != nil { continue } keys = append(keys, v) } sort.Ints(keys) return keys } func aggregateProgression(allProgression []db.ProgressionRecord, userChars map[string][]db.Character) map[string]map[int]float64 { result := make(map[string]map[int]float64) tomestoneToUser := make(map[int64]string) for discordID, chars := range userChars { for _, c := range chars { if c.TomestoneID.Valid { tomestoneToUser[c.TomestoneID.Int64] = discordID } } } for _, rec := range allProgression { discordID, ok := tomestoneToUser[rec.TomestoneID] if !ok { continue } if result[discordID] == nil { result[discordID] = make(map[int]float64) } current, exists := result[discordID][rec.EncounterID] if !exists || tomestone.IsBetter(rec.BestValue, current) { result[discordID][rec.EncounterID] = rec.BestValue } } return result } func chunkPlayers(ids []string, userChars map[string][]db.Character, pageSize int) [][]string { var pages [][]string for i := 0; i < len(ids); i += pageSize { end := i + pageSize if end > len(ids) { end = len(ids) } pages = append(pages, ids[i:end]) } return pages } func formatPage(playerIDs []string, userChars map[string][]db.Character, aggProgression map[string]map[int]float64, encounterOrder []int, activityIDToLabel map[string]string, page, totalPages int) string { var b strings.Builder if page <= 1 { b.WriteString("# Player Roster\n\n") } for _, id := range playerIDs { name := formatDisplayName(id) charList := formatCharList(userChars[id]) b.WriteString(fmt.Sprintf("%s — %s\n", name, charList)) if prog, ok := aggProgression[id]; ok && len(prog) > 0 { progStr := formatProgressionRow(prog, encounterOrder, activityIDToLabel) b.WriteString(fmt.Sprintf(" %s\n", progStr)) } b.WriteString("\n") } return b.String() } func formatProgressionRow(prog map[int]float64, encounterOrder []int, activityIDToLabel map[string]string) string { var parts []string for _, encounterID := range encounterOrder { if val, ok := prog[encounterID]; ok { label := activityIDToLabel[strconv.Itoa(encounterID)] parts = append(parts, fmt.Sprintf("%s: %s", label, tomestone.FormatProgression(val))) } } return strings.Join(parts, ", ") } func formatDisplayName(discordID string) string { return fmt.Sprintf("<@%s>", discordID) } func formatCharList(chars []db.Character) string { if len(chars) == 0 { return "no characters" } var parts []string for _, c := range chars { parts = append(parts, fmt.Sprintf("%s %s (%s)", c.FirstName, c.LastName, c.ServerName)) } return strings.Join(parts, ", ") } func countAllPlayers(userChars map[string][]db.Character) int { return len(userChars) }