OOKNET                             [ /  search the index  ]  
──────────────────────────────────────────────────────────────────────────────────────
══════════════════════════════════════════════════════════════════════════════════════
OOKNET   [ /  search  ]  
────────────────────────────────────────────────
════════════════════════════════════════════════
 
 
master @ 322 LINES
 
[ HISTORY ]  [ UP ]
 

package generator

import (
	"database/sql"
	"fmt"
	"ookstats/internal/loader"
	"ookstats/internal/wow"
	"ookstats/internal/writer"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"sync"
	"sync/atomic"
	"time"
)

type accountLeaderboardJob struct {
	seasonID  int
	scope     string // "global" | "regional" | "realm"
	region    string // for regional / realm
	realmSlug string // for realm
	out       string
	pageSize  int
}

func GenerateAccountLeaderboards(db *sql.DB, out string, pageSize int, regions []s
tring, workers int) error {
	if pageSize <= 0 {
		pageSize = 25
	}
	if workers <= 0 {
		workers = 4
	}
	if len(regions) == 0 {
		regions = []string{"us", "eu", "kr", "tw"}
	}

	seasons, err := loadSeasons(db)
	if err != nil {
		return err
	}
	if len(seasons) == 0 {
		fmt.Println("Warning: No seasons found - skipping account leaderboards")
		return nil
	}

	// realm-scoped: account placed where its main lives (with connected-realm rollup
)
	realmSlugs := make(map[string][]string, len(regions))
	for _, reg := range regions {
		slugs, err := loadParentRealmSlugs(db, reg)
		if err != nil {
			return err
		}
		realmSlugs[reg] = slugs
	}
	realmParents, err := loader.LoadRealmParents(db, regions)
	if err != nil {
		return err
	}

	// share per-season load across scope workers
	type cached struct {
		aggregates []*loader.AccountAggregate
		err        error
	}
	cache := make(map[int]*cached)
	var cacheMu sync.Mutex
	getAggregates := func(seasonID int) ([]*loader.AccountAggregate, error) {
		cacheMu.Lock()
		c, ok := cache[seasonID]
		cacheMu.Unlock()
		if ok {
			return c.aggregates, c.err
		}
		aggs, err := loader.LoadAccountAggregates(db, seasonID)
		cacheMu.Lock()
		cache[seasonID] = &cached{aggregates: aggs, err: err}
		cacheMu.Unlock()
		return aggs, err
	}

	totalJobs := 0
	for range seasons {
		totalJobs += 1            // global
		totalJobs += len(regions) // regional
		for _, reg := range regions {
			totalJobs += len(realmSlugs[reg]) // realm
		}
	}
	fmt.Printf("Generating account leaderboards with %d workers (%d total jobs)...\n"
, workers, totalJobs)

	jobs := make(chan accountLeaderboardJob, 100)
	var firstErr atomic.Value
	var completed atomic.Int64
	var wg sync.WaitGroup

	for i := 0; i < workers; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for job := range jobs {
				if firstErr.Load() != nil {
					continue
				}
				aggs, err := getAggregates(job.seasonID)
				if err != nil {
					firstErr.CompareAndSwap(nil, err)
					continue
				}
				if err := writeAccountScope(job, aggs, realmParents); err != nil {
					firstErr.CompareAndSwap(nil, err)
					continue
				}
				c := completed.Add(1)
				if c%10 == 0 {
					fmt.Printf("  ... %d/%d account leaderboards generated\n", c, totalJobs)
				}
			}
		}()
	}

	for _, season := range seasons {
		fmt.Printf("\n=== Queuing Account Leaderboards for Season %d (%s) ===\n", season
.ID, season.Name)
		seasonOut := filepath.Join(out, "season", fmt.Sprintf("%d", season.ID))

		jobs <- accountLeaderboardJob{seasonID: season.ID, scope: "global", out: seasonO
ut, pageSize: pageSize}
		for _, reg := range regions {
			jobs <- accountLeaderboardJob{seasonID: season.ID, scope: "regional", region: r
eg, out: seasonOut, pageSize: pageSize}
		}
		for _, reg := range regions {
			for _, rslug := range realmSlugs[reg] {
				jobs <- accountLeaderboardJob{seasonID: season.ID, scope: "realm", region: reg
, realmSlug: rslug, out: seasonOut, pageSize: pageSize}
			}
		}
	}
	close(jobs)
	wg.Wait()

	if err := firstErr.Load(); err != nil {
		return err.(error)
	}
	fmt.Printf("\n[OK] Generated %d account leaderboards\n", completed.Load())
	return nil
}

func writeAccountScope(job accountLeaderboardJob, all []*loader.AccountAggregate, 
realmParents map[string]string) error {
	type accountRow struct {
		Aggregate *loader.AccountAggregate
		Combined  int64
		Main      *loader.AccountChar
	}
	rows := make([]accountRow, 0, len(all))
	for _, agg := range all {
		if (job.scope == "regional" || job.scope == "realm") && !strings.EqualFold(agg.R
egion, job.region) {
			continue
		}
		if len(agg.BestPerDungeon) < loader.DungeonsForFullCoverage {
			continue
		}
		main := loader.PickMainCharacter(agg)
		if job.scope == "realm" {
			if main == nil {
				continue
			}
			parent := loader.ResolveParentRealm(realmParents, main.Region, main.RealmSlug)
			if !strings.EqualFold(parent, job.realmSlug) {
				continue
			}
		}
		var sum int64
		for _, br := range agg.BestPerDungeon {
			sum += br.Duration
		}
		rows = append(rows, accountRow{Aggregate: agg, Combined: sum, Main: main})
	}

	sort.Slice(rows, func(i, j int) bool {
		if rows[i].Combined != rows[j].Combined {
			return rows[i].Combined < rows[j].Combined
		}
		return rows[i].Aggregate.AccountID < rows[j].Aggregate.AccountID
	})

	dir := filepath.Join(job.out, "accounts", job.scope)
	switch job.scope {
	case "regional":
		dir = filepath.Join(job.out, "accounts", "regional", job.region)
	case "realm":
		dir = filepath.Join(job.out, "accounts", "realm", job.region, job.realmSlug)
	}
	if err := os.MkdirAll(dir, 0o755); err != nil {
		return err
	}

	total := len(rows)
	pages := (total + job.pageSize - 1) / job.pageSize
	if pages == 0 {
		pages = 1
	}
	for p := 1; p <= pages; p++ {
		start := (p - 1) * job.pageSize
		end := start + job.pageSize
		if end > total {
			end = total
		}
		out := make([]map[string]any, 0, end-start)
		for i := start; i < end; i++ {
			rank := i + 1
			bracket := loader.PercentileBracket(rank, total)
			out = append(out, buildAccountRow(rank, bracket, rows[i].Aggregate, rows[i].Mai
n, rows[i].Combined))
		}
		title := "Global Account Rankings"
		switch job.scope {
		case "regional":
			title = strings.ToUpper(job.region) + " Account Rankings"
		case "realm":
			title = strings.ToUpper(job.region) + " " + job.realmSlug + " Account Rankings"
		}
		page := buildAccountLeaderboardPage(out, title, total, pages, p, job.pageSize)
		if err := writer.WriteJSONFileCompact(filepath.Join(dir, fmt.Sprintf("%d.json", 
p)), page); err != nil {
			return err
		}
	}
	return nil
}

// caller picks main so the realm filter and rendered face stay in lockstep
func buildAccountRow(rank int, bracket string, agg *loader.AccountAggregate, main 
*loader.AccountChar, combined int64) map[string]any {
	alts := make([]map[string]any, 0, len(agg.Chars)-1)
	for _, c := range agg.Chars {
		if main != nil && c.PlayerID == main.PlayerID {
			continue
		}
		// phantom alt
		if !c.CombinedBestTime.Valid && c.ClassName == "" && c.TotalRuns == 0 {
			continue
		}
		alts = append(alts, charSummary(c))
	}
	sort.Slice(alts, func(i, j int) bool {
		na, _ := alts[i]["name"].(string)
		nb, _ := alts[j]["name"].(string)
		return na < nb
	})

	playerName := make(map[int64]string, len(agg.Chars))
	playerRealm := make(map[int64]string, len(agg.Chars))
	for _, c := range agg.Chars {
		playerName[c.PlayerID] = c.Name
		playerRealm[c.PlayerID] = c.RealmSlug
	}
	perDungeon := make(map[string]map[string]any, len(agg.BestPerDungeon))
	for did, br := range agg.BestPerDungeon {
		perDungeon[fmt.Sprintf("%d", did)] = map[string]any{
			"duration_ms":        br.Duration,
			"by_character":       playerName[br.PlayerID],
			"by_character_realm": playerRealm[br.PlayerID],
		}
	}

	totalRuns := 0
	for _, c := range agg.Chars {
		totalRuns += c.TotalRuns
	}

	return map[string]any{
		"rank":                       rank,
		"bracket":                    bracket,
		"account_id":                 agg.AccountID,
		"region":                     agg.Region,
		"account_combined_best_time": combined,
		"dungeons_completed":         len(agg.BestPerDungeon),
		"total_runs":                 totalRuns,
		"main":                       charSummary(main),
		"alts":                       alts,
		"per_dungeon_best":           perDungeon,
	}
}

func charSummary(c *loader.AccountChar) map[string]any {
	if c == nil {
		return nil
	}
	obj := map[string]any{
		"player_id":         c.PlayerID,
		"name":              c.Name,
		"realm_slug":        c.RealmSlug,
		"realm_name":        c.RealmName,
		"region":            c.Region,
		"class_name":        c.ClassName,
		"active_spec_name":  c.ActiveSpecName,
		"total_runs":        c.TotalRuns,
		"has_full_coverage": c.HasFullCoverage,
	}
	if c.CombinedBestTime.Valid {
		obj["combined_best_time"] = c.CombinedBestTime.Int64
	}
	if c.MainSpecID.Valid {
		v := int(c.MainSpecID.Int64)
		obj["class_name"], obj["active_spec_name"] = wow.FallbackClassAndSpec(c.ClassNam
e, c.ActiveSpecName, &v)
		obj["main_spec_id"] = v
	}
	return obj
}

func buildAccountLeaderboardPage(rows []map[string]any, title string, total, total
Pages, currentPage, pageSize int) map[string]any {
	return map[string]any{
		"leaderboard":         rows,
		"title":               title,
		"generated_timestamp": time.Now().UnixMilli(),
		"pagination": map[string]any{
			"currentPage":   currentPage,
			"pageSize":      pageSize,
			"totalAccounts": total,
			"totalPages":    totalPages,
			"hasNextPage":   currentPage < totalPages,
			"hasPrevPage":   currentPage > 1,
			"totalRuns":     total, // frontend compatibility
		},
	}
}

package generator

import (
	"database/sql"
	"fmt"
	"ookstats/internal/loader"
	"ookstats/internal/wow"
	"ookstats/internal/writer"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"sync"
	"sync/atomic"
	"time"
)

type accountLeaderboardJob struct {
	seasonID  int
	scope     string // "global" | "regional" |
 "realm"
	region    string // for regional / realm
	realmSlug string // for realm
	out       string
	pageSize  int
}

func GenerateAccountLeaderboards(db *sql.DB,
 out string, pageSize int, regions []string,
 workers int) error {
	if pageSize <= 0 {
		pageSize = 25
	}
	if workers <= 0 {
		workers = 4
	}
	if len(regions) == 0 {
		regions = []string{"us", "eu", "kr", "tw"}
	}

	seasons, err := loadSeasons(db)
	if err != nil {
		return err
	}
	if len(seasons) == 0 {
		fmt.Println("Warning: No seasons found - s
kipping account leaderboards")
		return nil
	}

	// realm-scoped: account placed where its m
ain lives (with connected-realm rollup)
	realmSlugs := make(map[string][]string, len
(regions))
	for _, reg := range regions {
		slugs, err := loadParentRealmSlugs(db, reg
)
		if err != nil {
			return err
		}
		realmSlugs[reg] = slugs
	}
	realmParents, err := loader.LoadRealmParent
s(db, regions)
	if err != nil {
		return err
	}

	// share per-season load across scope worke
rs
	type cached struct {
		aggregates []*loader.AccountAggregate
		err        error
	}
	cache := make(map[int]*cached)
	var cacheMu sync.Mutex
	getAggregates := func(seasonID int) ([]*loa
der.AccountAggregate, error) {
		cacheMu.Lock()
		c, ok := cache[seasonID]
		cacheMu.Unlock()
		if ok {
			return c.aggregates, c.err
		}
		aggs, err := loader.LoadAccountAggregates(
db, seasonID)
		cacheMu.Lock()
		cache[seasonID] = &cached{aggregates: aggs
, err: err}
		cacheMu.Unlock()
		return aggs, err
	}

	totalJobs := 0
	for range seasons {
		totalJobs += 1            // global
		totalJobs += len(regions) // regional
		for _, reg := range regions {
			totalJobs += len(realmSlugs[reg]) // real
m
		}
	}
	fmt.Printf("Generating account leaderboards
 with %d workers (%d total jobs)...\n", work
ers, totalJobs)

	jobs := make(chan accountLeaderboardJob, 10
0)
	var firstErr atomic.Value
	var completed atomic.Int64
	var wg sync.WaitGroup

	for i := 0; i < workers; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for job := range jobs {
				if firstErr.Load() != nil {
					continue
				}
				aggs, err := getAggregates(job.seasonID)
				if err != nil {
					firstErr.CompareAndSwap(nil, err)
					continue
				}
				if err := writeAccountScope(job, aggs, r
ealmParents); err != nil {
					firstErr.CompareAndSwap(nil, err)
					continue
				}
				c := completed.Add(1)
				if c%10 == 0 {
					fmt.Printf("  ... %d/%d account leaderb
oards generated\n", c, totalJobs)
				}
			}
		}()
	}

	for _, season := range seasons {
		fmt.Printf("\n=== Queuing Account Leaderbo
ards for Season %d (%s) ===\n", season.ID, s
eason.Name)
		seasonOut := filepath.Join(out, "season", 
fmt.Sprintf("%d", season.ID))

		jobs <- accountLeaderboardJob{seasonID: se
ason.ID, scope: "global", out: seasonOut, pa
geSize: pageSize}
		for _, reg := range regions {
			jobs <- accountLeaderboardJob{seasonID: s
eason.ID, scope: "regional", region: reg, ou
t: seasonOut, pageSize: pageSize}
		}
		for _, reg := range regions {
			for _, rslug := range realmSlugs[reg] {
				jobs <- accountLeaderboardJob{seasonID: 
season.ID, scope: "realm", region: reg, real
mSlug: rslug, out: seasonOut, pageSize: page
Size}
			}
		}
	}
	close(jobs)
	wg.Wait()

	if err := firstErr.Load(); err != nil {
		return err.(error)
	}
	fmt.Printf("\n[OK] Generated %d account lea
derboards\n", completed.Load())
	return nil
}

func writeAccountScope(job accountLeaderboar
dJob, all []*loader.AccountAggregate, realmP
arents map[string]string) error {
	type accountRow struct {
		Aggregate *loader.AccountAggregate
		Combined  int64
		Main      *loader.AccountChar
	}
	rows := make([]accountRow, 0, len(all))
	for _, agg := range all {
		if (job.scope == "regional" || job.scope =
= "realm") && !strings.EqualFold(agg.Region,
 job.region) {
			continue
		}
		if len(agg.BestPerDungeon) < loader.Dungeo
nsForFullCoverage {
			continue
		}
		main := loader.PickMainCharacter(agg)
		if job.scope == "realm" {
			if main == nil {
				continue
			}
			parent := loader.ResolveParentRealm(realm
Parents, main.Region, main.RealmSlug)
			if !strings.EqualFold(parent, job.realmSl
ug) {
				continue
			}
		}
		var sum int64
		for _, br := range agg.BestPerDungeon {
			sum += br.Duration
		}
		rows = append(rows, accountRow{Aggregate: 
agg, Combined: sum, Main: main})
	}

	sort.Slice(rows, func(i, j int) bool {
		if rows[i].Combined != rows[j].Combined {
			return rows[i].Combined < rows[j].Combine
d
		}
		return rows[i].Aggregate.AccountID < rows[
j].Aggregate.AccountID
	})

	dir := filepath.Join(job.out, "accounts", j
ob.scope)
	switch job.scope {
	case "regional":
		dir = filepath.Join(job.out, "accounts", "
regional", job.region)
	case "realm":
		dir = filepath.Join(job.out, "accounts", "
realm", job.region, job.realmSlug)
	}
	if err := os.MkdirAll(dir, 0o755); err != n
il {
		return err
	}

	total := len(rows)
	pages := (total + job.pageSize - 1) / job.p
ageSize
	if pages == 0 {
		pages = 1
	}
	for p := 1; p <= pages; p++ {
		start := (p - 1) * job.pageSize
		end := start + job.pageSize
		if end > total {
			end = total
		}
		out := make([]map[string]any, 0, end-start
)
		for i := start; i < end; i++ {
			rank := i + 1
			bracket := loader.PercentileBracket(rank,
 total)
			out = append(out, buildAccountRow(rank, b
racket, rows[i].Aggregate, rows[i].Main, row
s[i].Combined))
		}
		title := "Global Account Rankings"
		switch job.scope {
		case "regional":
			title = strings.ToUpper(job.region) + " A
ccount Rankings"
		case "realm":
			title = strings.ToUpper(job.region) + " "
 + job.realmSlug + " Account Rankings"
		}
		page := buildAccountLeaderboardPage(out, t
itle, total, pages, p, job.pageSize)
		if err := writer.WriteJSONFileCompact(file
path.Join(dir, fmt.Sprintf("%d.json", p)), p
age); err != nil {
			return err
		}
	}
	return nil
}

// caller picks main so the realm filter and
 rendered face stay in lockstep
func buildAccountRow(rank int, bracket strin
g, agg *loader.AccountAggregate, main *loade
r.AccountChar, combined int64) map[string]an
y {
	alts := make([]map[string]any, 0, len(agg.C
hars)-1)
	for _, c := range agg.Chars {
		if main != nil && c.PlayerID == main.Playe
rID {
			continue
		}
		// phantom alt
		if !c.CombinedBestTime.Valid && c.ClassNam
e == "" && c.TotalRuns == 0 {
			continue
		}
		alts = append(alts, charSummary(c))
	}
	sort.Slice(alts, func(i, j int) bool {
		na, _ := alts[i]["name"].(string)
		nb, _ := alts[j]["name"].(string)
		return na < nb
	})

	playerName := make(map[int64]string, len(ag
g.Chars))
	playerRealm := make(map[int64]string, len(a
gg.Chars))
	for _, c := range agg.Chars {
		playerName[c.PlayerID] = c.Name
		playerRealm[c.PlayerID] = c.RealmSlug
	}
	perDungeon := make(map[string]map[string]an
y, len(agg.BestPerDungeon))
	for did, br := range agg.BestPerDungeon {
		perDungeon[fmt.Sprintf("%d", did)] = map[s
tring]any{
			"duration_ms":        br.Duration,
			"by_character":       playerName[br.Playe
rID],
			"by_character_realm": playerRealm[br.Play
erID],
		}
	}

	totalRuns := 0
	for _, c := range agg.Chars {
		totalRuns += c.TotalRuns
	}

	return map[string]any{
		"rank":                       rank,
		"bracket":                    bracket,
		"account_id":                 agg.AccountI
D,
		"region":                     agg.Region,
		"account_combined_best_time": combined,
		"dungeons_completed":         len(agg.Best
PerDungeon),
		"total_runs":                 totalRuns,
		"main":                       charSummary(
main),
		"alts":                       alts,
		"per_dungeon_best":           perDungeon,
	}
}

func charSummary(c *loader.AccountChar) map[
string]any {
	if c == nil {
		return nil
	}
	obj := map[string]any{
		"player_id":         c.PlayerID,
		"name":              c.Name,
		"realm_slug":        c.RealmSlug,
		"realm_name":        c.RealmName,
		"region":            c.Region,
		"class_name":        c.ClassName,
		"active_spec_name":  c.ActiveSpecName,
		"total_runs":        c.TotalRuns,
		"has_full_coverage": c.HasFullCoverage,
	}
	if c.CombinedBestTime.Valid {
		obj["combined_best_time"] = c.CombinedBest
Time.Int64
	}
	if c.MainSpecID.Valid {
		v := int(c.MainSpecID.Int64)
		obj["class_name"], obj["active_spec_name"]
 = wow.FallbackClassAndSpec(c.ClassName, c.A
ctiveSpecName, &v)
		obj["main_spec_id"] = v
	}
	return obj
}

func buildAccountLeaderboardPage(rows []map[
string]any, title string, total, totalPages,
 currentPage, pageSize int) map[string]any {
	return map[string]any{
		"leaderboard":         rows,
		"title":               title,
		"generated_timestamp": time.Now().UnixMill
i(),
		"pagination": map[string]any{
			"currentPage":   currentPage,
			"pageSize":      pageSize,
			"totalAccounts": total,
			"totalPages":    totalPages,
			"hasNextPage":   currentPage < totalPages
,
			"hasPrevPage":   currentPage > 1,
			"totalRuns":     total, // frontend compa
tibility
		},
	}
}
 
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET