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

package generator

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

// playerLeaderboardJob represents a single player leaderboard generation task
type playerLeaderboardJob struct {
	seasonID   int
	seasonName string
	scope      string // "global", "regional", "realm", "class"
	region     string // for regional/realm/class scopes
	realmSlug  string // for realm scope
	classKey   string // for class scope
	out        string
	pageSize   int
	regions    []string // for class scope
}

// GeneratePlayerLeaderboards generates player ranking JSON files for all scopes p
er season
// Uses a worker pool for parallel generation
func GeneratePlayerLeaderboards(db *sql.DB, out string, pageSize int, regions []st
ring, workers int) error {
	if pageSize <= 0 {
		pageSize = 25
	}
	if workers <= 0 {
		workers = 10
	}

	// Default regions
	if len(regions) == 0 {
		regions = []string{"us", "eu", "kr", "tw"}
	}

	// Load seasons
	seasons, err := loadSeasons(db)
	if err != nil {
		return err
	}

	if len(seasons) == 0 {
		fmt.Println("Warning: No seasons found - skipping player leaderboard generation"
)
		return nil
	}

	// Pre-load realm slugs per region (only parent realms)
	realmSlugs := make(map[string][]string)
	for _, reg := range regions {
		slugs, err := loadParentRealmSlugs(db, reg)
		if err != nil {
			return err
		}
		realmSlugs[reg] = slugs
	}

	classKeys := []string{"death_knight", "druid", "hunter", "mage", "monk", "paladin
", "priest", "rogue", "shaman", "warlock", "warrior"}

	// Count total jobs
	var totalJobs int
	for range seasons {
		totalJobs += 1            // global
		totalJobs += len(regions) // regional
		for _, reg := range regions {
			totalJobs += len(realmSlugs[reg]) // realm
		}
		totalJobs += len(classKeys) // class (each handles all scopes internally)
	}

	fmt.Printf("Generating player leaderboards with %d workers (%d total jobs)...\n",
 workers, totalJobs)

	// Create job channel
	jobs := make(chan playerLeaderboardJob, 100)
	var firstErr atomic.Value
	var completed atomic.Int64
	var wg sync.WaitGroup

	// Start workers
	for i := 0; i < workers; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for job := range jobs {
				if firstErr.Load() != nil {
					continue
				}

				var err error
				switch job.scope {
				case "global":
					err = generateGlobalPlayerLeaderboard(db, job.out, job.pageSize, job.seasonID
)
				case "regional":
					err = generateRegionalPlayerLeaderboard(db, job.out, job.region, job.pageSize
, job.seasonID)
				case "realm":
					err = generateSingleRealmPlayerLeaderboard(db, job.out, job.region, job.realm
Slug, job.pageSize, job.seasonID)
				case "class":
					err = generateClassPlayerLeaderboards(db, job.out, job.classKey, job.pageSize
, job.regions, job.seasonID)
				}

				if err != nil {
					firstErr.CompareAndSwap(nil, err)
					continue
				}

				c := completed.Add(1)
				if c%50 == 0 {
					fmt.Printf("  ... %d/%d player leaderboards generated\n", c, totalJobs)
				}
			}
		}()
	}

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

		// Global
		jobs <- playerLeaderboardJob{
			seasonID: season.ID,
			scope:    "global",
			out:      seasonOut,
			pageSize: pageSize,
		}

		// Regional
		for _, reg := range regions {
			jobs <- playerLeaderboardJob{
				seasonID: season.ID,
				scope:    "regional",
				region:   reg,
				out:      seasonOut,
				pageSize: pageSize,
			}
		}

		// Realm
		for _, reg := range regions {
			for _, rslug := range realmSlugs[reg] {
				jobs <- playerLeaderboardJob{
					seasonID:  season.ID,
					scope:     "realm",
					region:    reg,
					realmSlug: rslug,
					out:       seasonOut,
					pageSize:  pageSize,
				}
			}
		}

		// Class (each handles global/regional/realm internally)
		for _, cls := range classKeys {
			jobs <- playerLeaderboardJob{
				seasonID: season.ID,
				scope:    "class",
				classKey: cls,
				out:      seasonOut,
				pageSize: pageSize,
				regions:  regions,
			}
		}
	}
	close(jobs)

	wg.Wait()

	if err := firstErr.Load(); err != nil {
		return err.(error)
	}

	fmt.Printf("\n[OK] Generated %d player leaderboards\n", completed.Load())
	return nil
}

func generateGlobalPlayerLeaderboard(db *sql.DB, out string, pageSize int, seasonI
D int) error {
	dir := filepath.Join(out, "players", "global")
	if err := os.MkdirAll(dir, 0o755); err != nil {
		return err
	}
	rows, err := db.Query(`
		SELECT p.id, p.name, r.slug, r.name, r.region,
			   COALESCE(pd.class_name,''), COALESCE(pd.active_spec_name,''), pp.main_spec_i
d,
			   pp.combined_best_time, pp.dungeons_completed, pp.total_runs,
			   COALESCE(pp.global_ranking_bracket, '')
		FROM players p
		JOIN realms r ON p.realm_id = r.id
		JOIN player_profiles pp ON p.id = pp.player_id
		LEFT JOIN player_details pd ON p.id = pd.player_id
		WHERE pp.season_id = ? AND pp.has_complete_coverage = 1 AND pp.combined_best_tim
e IS NOT NULL
		ORDER BY pp.combined_best_time ASC, p.name ASC
	`, seasonID)
	if err != nil {
		return fmt.Errorf("players query (global, season %d): %w", seasonID, err)
	}
	all, err := scanPlayerRows(rows)
	if err != nil {
		return err
	}
	return writePlayerLeaderboardPages(dir, all, "Global Player Rankings", pageSize)
}

func generateRegionalPlayerLeaderboard(db *sql.DB, out, region string, pageSize in
t, seasonID int) error {
	dir := filepath.Join(out, "players", "regional", region)
	if err := os.MkdirAll(dir, 0o755); err != nil {
		return err
	}
	rows, err := db.Query(`
		SELECT p.id, p.name, r.slug, r.name, r.region,
			   COALESCE(pd.class_name,''), COALESCE(pd.active_spec_name,''), pp.main_spec_i
d,
			   pp.combined_best_time, pp.dungeons_completed, pp.total_runs,
			   COALESCE(pp.regional_ranking_bracket, '')
		FROM players p
		JOIN realms r ON p.realm_id = r.id
		JOIN player_profiles pp ON p.id = pp.player_id
		LEFT JOIN player_details pd ON p.id = pd.player_id
		WHERE pp.season_id = ? AND r.region = ? AND pp.has_complete_coverage = 1 AND pp.
combined_best_time IS NOT NULL
		ORDER BY pp.combined_best_time ASC, p.name ASC
	`, seasonID, region)
	if err != nil {
		return fmt.Errorf("players query (regional, season %d): %w", seasonID, err)
	}
	all, err := scanPlayerRows(rows)
	if err != nil {
		return err
	}
	title := strings.ToUpper(region) + " Player Rankings"
	return writePlayerLeaderboardPages(dir, all, title, pageSize)
}

// pool = parent + child realms (matches per-season realm leaderboard)
func generateSingleRealmPlayerLeaderboard(db *sql.DB, out, region, rslug string, p
ageSize int, seasonID int) error {
	dir := filepath.Join(out, "players", "realm", region, rslug)
	if err := os.MkdirAll(dir, 0o755); err != nil {
		return err
	}
	rows, err := db.Query(`
		SELECT p.id, p.name, r.slug, r.name, r.region,
			   COALESCE(pd.class_name,''), COALESCE(pd.active_spec_name,''), pp.main_spec_i
d,
			   pp.combined_best_time, pp.dungeons_completed, pp.total_runs,
			   COALESCE(pp.realm_ranking_bracket, '')
		FROM players p
		JOIN realms r ON p.realm_id = r.id
		JOIN player_profiles pp ON p.id = pp.player_id
		LEFT JOIN player_details pd ON p.id = pd.player_id
		WHERE pp.season_id = ? AND r.region = ?
			AND (r.slug = ? OR r.parent_realm_slug = ?)
			AND pp.has_complete_coverage = 1 AND pp.combined_best_time IS NOT NULL
		ORDER BY pp.combined_best_time ASC, p.name ASC
	`, seasonID, region, rslug, rslug)
	if err != nil {
		return fmt.Errorf("players query (realm, season %d): %w", seasonID, err)
	}
	all, err := scanPlayerRows(rows)
	if err != nil {
		return err
	}
	title := strings.ToUpper(region) + "/" + rslug + " Player Rankings"
	return writePlayerLeaderboardPages(dir, all, title, pageSize)
}

func writePlayerLeaderboardPages(dir string, all []map[string]any, title string, p
ageSize int) error {
	total := len(all)
	pages := (total + pageSize - 1) / pageSize
	for p := 1; p <= pages; p++ {
		start := (p - 1) * pageSize
		end := start + pageSize
		if end > total {
			end = total
		}
		page := buildPlayerLeaderboardPage(all[start:end], title, total, pages, p, pageS
ize)
		if err := writer.WriteJSONFileCompact(filepath.Join(dir, fmt.Sprintf("%d.json", 
p)), page); err != nil {
			return err
		}
	}
	return nil
}

// generateClassPlayerLeaderboards generates class-filtered player rankings for a 
season
func generateClassPlayerLeaderboards(db *sql.DB, out, classKey string, pageSize in
t, regions []string, seasonID int) error {
	// Global class leaderboard
	if err := generateClassScope(db, out, "global", "", "", classKey, pageSize, seaso
nID); err != nil {
		return err
	}

	// Regional class leaderboards
	for _, reg := range regions {
		if err := generateClassScope(db, out, "regional", reg, "", classKey, pageSize, s
easonID); err != nil {
			return err
		}

		// Realm class leaderboards - only for parent/independent realms (skip child rea
lms)
		slugs, err := loadParentRealmSlugs(db, reg)
		if err != nil {
			return err
		}

		for _, rslug := range slugs {
			if err := generateClassScope(db, out, "realm", reg, rslug, classKey, pageSize, 
seasonID); err != nil {
				return err
			}
		}
	}
	return nil
}

// generateClassScope generates a class-filtered leaderboard for a specific scope 
and season
func generateClassScope(db *sql.DB, out, scope, region, realmSlug, classKey string
, pageSize int, seasonID int) error {
	var dir string
	if scope == "global" {
		dir = filepath.Join(out, "players", "class", classKey, "global")
	} else if scope == "regional" {
		dir = filepath.Join(out, "players", "class", classKey, "regional", region)
	} else {
		dir = filepath.Join(out, "players", "class", classKey, "realm", region, realmSlu
g)
	}
	if err := os.MkdirAll(dir, 0o755); err != nil {
		return err
	}

	// Fetch ALL players for this season, then filter by class in Go (to handle fallb
ack logic)
	var rows *sql.Rows
	var err error
	if scope == "global" {
		rows, err = db.Query(`
			SELECT p.id, p.name, r.slug, r.name, r.region,
				   COALESCE(pd.class_name,''), COALESCE(pd.active_spec_name,''), pp.main_spec_
id,
				   pp.combined_best_time, pp.dungeons_completed, pp.total_runs,
				   COALESCE(pp.global_class_bracket, '')
			FROM players p
			JOIN realms r ON p.realm_id = r.id
			JOIN player_profiles pp ON p.id = pp.player_id
			LEFT JOIN player_details pd ON p.id = pd.player_id
			WHERE pp.season_id = ? AND pp.has_complete_coverage = 1 AND pp.combined_best_ti
me IS NOT NULL
				AND pp.class_name IS NOT NULL
			ORDER BY pp.combined_best_time ASC, p.name ASC
		`, seasonID)
	} else if scope == "regional" {
		rows, err = db.Query(`
			SELECT p.id, p.name, r.slug, r.name, r.region,
				   COALESCE(pd.class_name,''), COALESCE(pd.active_spec_name,''), pp.main_spec_
id,
				   pp.combined_best_time, pp.dungeons_completed, pp.total_runs,
				   COALESCE(pp.region_class_bracket, '')
			FROM players p
			JOIN realms r ON p.realm_id = r.id
			JOIN player_profiles pp ON p.id = pp.player_id
			LEFT JOIN player_details pd ON p.id = pd.player_id
			WHERE pp.season_id = ? AND r.region = ? AND pp.has_complete_coverage = 1 AND pp
.combined_best_time IS NOT NULL
				AND pp.class_name IS NOT NULL
			ORDER BY pp.combined_best_time ASC, p.name ASC
		`, seasonID, region)
	} else {
		// Realm scope - include entire pool (parent + all children)
		rows, err = db.Query(`
			SELECT p.id, p.name, r.slug, r.name, r.region,
				   COALESCE(pd.class_name,''), COALESCE(pd.active_spec_name,''), pp.main_spec_
id,
				   pp.combined_best_time, pp.dungeons_completed, pp.total_runs,
				   COALESCE(pp.realm_class_bracket, '')
			FROM players p
			JOIN realms r ON p.realm_id = r.id
			JOIN player_profiles pp ON p.id = pp.player_id
			LEFT JOIN player_details pd ON p.id = pd.player_id
			WHERE pp.season_id = ? AND r.region = ?
				AND (r.slug = ? OR r.parent_realm_slug = ?)
				AND pp.has_complete_coverage = 1 AND pp.combined_best_time IS NOT NULL
				AND pp.class_name IS NOT NULL
			ORDER BY pp.combined_best_time ASC, p.name ASC
		`, seasonID, region, realmSlug, realmSlug)
	}
	if err != nil {
		return err
	}
	defer rows.Close()

	type PlayerRow struct {
		ID                                                            int64
		Name, RealmSlug, RealmName, Region, ClassName, ActiveSpecName string
		MainSpecID                                                    sql.NullInt64
		CombinedBestTime                                              sql.NullInt64
		DungeonsCompleted, TotalRuns                                  int
		RankingBracket                                                string
	}

	// Collect all matching players (filter by class in Go)
	var allMatching []PlayerRow
	for rows.Next() {
		var r PlayerRow
		if err := rows.Scan(&r.ID, &r.Name, &r.RealmSlug, &r.RealmName, &r.Region, &r.Cl
assName, &r.ActiveSpecName, &r.MainSpecID, &r.CombinedBestTime, &r.DungeonsComplet
ed, &r.TotalRuns, &r.RankingBracket); err != nil {
			return err
		}

		// Derive class from main_spec_id if missing
		derivedClass := r.ClassName
		derivedSpec := r.ActiveSpecName
		if (derivedClass == "" || derivedSpec == "") && r.MainSpecID.Valid {
			if cls, spec, ok := wow.GetClassAndSpec(int(r.MainSpecID.Int64)); ok {
				if derivedClass == "" {
					derivedClass = cls
				}
				if derivedSpec == "" {
					derivedSpec = spec
				}
			}
		}

		// Filter by class (case-insensitive, space -> underscore)
		playerClassKey := strings.ReplaceAll(strings.ToLower(derivedClass), " ", "_")
		if playerClassKey == classKey {
			r.ClassName = derivedClass
			r.ActiveSpecName = derivedSpec
			allMatching = append(allMatching, r)
		}
	}

	// Paginate the filtered results
	total := len(allMatching)
	pages := (total + pageSize - 1) / pageSize

	for p := 1; p <= pages; p++ {
		offset := (p - 1) * pageSize
		end := offset + pageSize
		if end > total {
			end = total
		}

		var list []map[string]any
		for _, r := range allMatching[offset:end] {
			obj := map[string]any{
				"player_id":          r.ID,
				"name":               r.Name,
				"realm_slug":         r.RealmSlug,
				"realm_name":         r.RealmName,
				"region":             r.Region,
				"class_name":         r.ClassName,
				"active_spec_name":   r.ActiveSpecName,
				"dungeons_completed": r.DungeonsCompleted,
				"total_runs":         r.TotalRuns,
			}
			if r.RankingBracket != "" {
				obj["ranking_percentile"] = r.RankingBracket
			}
			if r.MainSpecID.Valid {
				obj["main_spec_id"] = int(r.MainSpecID.Int64)
			}
			if r.CombinedBestTime.Valid {
				obj["combined_best_time"] = r.CombinedBestTime.Int64
			}
			list = append(list, obj)
		}

		title := "Global Player Rankings"
		if scope == "regional" {
			title = strings.ToUpper(region) + " Player Rankings"
		} else if scope == "realm" {
			title = strings.ToUpper(region) + "/" + realmSlug + " Player Rankings"
		}

		page := buildPlayerLeaderboardPage(list, title, total, pages, p, pageSize)
		if err := writer.WriteJSONFileCompact(filepath.Join(dir, fmt.Sprintf("%d.json", 
p)), page); err != nil {
			return err
		}
	}
	return nil
}

// scanPlayerRows scans player rows and applies class/spec fallback
func scanPlayerRows(rows *sql.Rows) ([]map[string]any, error) {
	defer rows.Close()

	type PlayerRow struct {
		ID                int64
		Name              string
		RealmSlug         string
		RealmName         string
		Region            string
		ClassName         string
		ActiveSpecName    string
		MainSpecID        sql.NullInt64
		CombinedBestTime  sql.NullInt64
		DungeonsCompleted int
		TotalRuns         int
		RankingBracket    string
	}

	var list []map[string]any
	for rows.Next() {
		var r PlayerRow
		if err := rows.Scan(&r.ID, &r.Name, &r.RealmSlug, &r.RealmName, &r.Region, &r.Cl
assName, &r.ActiveSpecName, &r.MainSpecID, &r.CombinedBestTime, &r.DungeonsComplet
ed, &r.TotalRuns, &r.RankingBracket); err != nil {
			return nil, err
		}

		obj := map[string]any{
			"player_id":          r.ID,
			"name":               r.Name,
			"realm_slug":         r.RealmSlug,
			"realm_name":         r.RealmName,
			"region":             r.Region,
			"class_name":         r.ClassName,
			"active_spec_name":   r.ActiveSpecName,
			"dungeons_completed": r.DungeonsCompleted,
			"total_runs":         r.TotalRuns,
		}

		if r.RankingBracket != "" {
			obj["ranking_percentile"] = r.RankingBracket
		}

		// Apply class/spec fallback if missing
		if r.MainSpecID.Valid {
			v := int(r.MainSpecID.Int64)
			obj["class_name"], obj["active_spec_name"] = wow.FallbackClassAndSpec(r.ClassNa
me, r.ActiveSpecName, &v)
			obj["main_spec_id"] = v
		}

		if r.CombinedBestTime.Valid {
			obj["combined_best_time"] = r.CombinedBestTime.Int64
		}
		list = append(list, obj)
	}
	return list, nil
}

// buildPlayerLeaderboardPage builds a player leaderboard page structure
func buildPlayerLeaderboardPage(leaderboard []map[string]any, title string, total,
 totalPages, currentPage, pageSize int) map[string]any {
	return map[string]any{
		"leaderboard":         leaderboard,
		"title":               title,
		"generated_timestamp": time.Now().UnixMilli(),
		"pagination": map[string]any{
			"currentPage":  currentPage,
			"pageSize":     pageSize,
			"totalPlayers": total,
			"totalPages":   totalPages,
			"hasNextPage":  currentPage < totalPages,
			"hasPrevPage":  currentPage > 1,
			"totalRuns":    total, // keep compatibility with frontend
		},
	}
}

package generator

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

// playerLeaderboardJob represents a single 
player leaderboard generation task
type playerLeaderboardJob struct {
	seasonID   int
	seasonName string
	scope      string // "global", "regional", 
"realm", "class"
	region     string // for regional/realm/cla
ss scopes
	realmSlug  string // for realm scope
	classKey   string // for class scope
	out        string
	pageSize   int
	regions    []string // for class scope
}

// GeneratePlayerLeaderboards generates play
er ranking JSON files for all scopes per sea
son
// Uses a worker pool for parallel generatio
n
func GeneratePlayerLeaderboards(db *sql.DB, 
out string, pageSize int, regions []string, 
workers int) error {
	if pageSize <= 0 {
		pageSize = 25
	}
	if workers <= 0 {
		workers = 10
	}

	// Default regions
	if len(regions) == 0 {
		regions = []string{"us", "eu", "kr", "tw"}
	}

	// Load seasons
	seasons, err := loadSeasons(db)
	if err != nil {
		return err
	}

	if len(seasons) == 0 {
		fmt.Println("Warning: No seasons found - s
kipping player leaderboard generation")
		return nil
	}

	// Pre-load realm slugs per region (only pa
rent realms)
	realmSlugs := make(map[string][]string)
	for _, reg := range regions {
		slugs, err := loadParentRealmSlugs(db, reg
)
		if err != nil {
			return err
		}
		realmSlugs[reg] = slugs
	}

	classKeys := []string{"death_knight", "drui
d", "hunter", "mage", "monk", "paladin", "pr
iest", "rogue", "shaman", "warlock", "warrio
r"}

	// Count total jobs
	var totalJobs int
	for range seasons {
		totalJobs += 1            // global
		totalJobs += len(regions) // regional
		for _, reg := range regions {
			totalJobs += len(realmSlugs[reg]) // real
m
		}
		totalJobs += len(classKeys) // class (each
 handles all scopes internally)
	}

	fmt.Printf("Generating player leaderboards 
with %d workers (%d total jobs)...\n", worke
rs, totalJobs)

	// Create job channel
	jobs := make(chan playerLeaderboardJob, 100
)
	var firstErr atomic.Value
	var completed atomic.Int64
	var wg sync.WaitGroup

	// Start workers
	for i := 0; i < workers; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for job := range jobs {
				if firstErr.Load() != nil {
					continue
				}

				var err error
				switch job.scope {
				case "global":
					err = generateGlobalPlayerLeaderboard(d
b, job.out, job.pageSize, job.seasonID)
				case "regional":
					err = generateRegionalPlayerLeaderboard
(db, job.out, job.region, job.pageSize, job.
seasonID)
				case "realm":
					err = generateSingleRealmPlayerLeaderbo
ard(db, job.out, job.region, job.realmSlug, 
job.pageSize, job.seasonID)
				case "class":
					err = generateClassPlayerLeaderboards(d
b, job.out, job.classKey, job.pageSize, job.
regions, job.seasonID)
				}

				if err != nil {
					firstErr.CompareAndSwap(nil, err)
					continue
				}

				c := completed.Add(1)
				if c%50 == 0 {
					fmt.Printf("  ... %d/%d player leaderbo
ards generated\n", c, totalJobs)
				}
			}
		}()
	}

	// Queue all jobs
	for _, season := range seasons {
		fmt.Printf("\n=== Queuing Player Leaderboa
rds for Season %d (%s) ===\n", season.ID, se
ason.Name)
		seasonOut := filepath.Join(out, "season", 
fmt.Sprintf("%d", season.ID))

		// Global
		jobs <- playerLeaderboardJob{
			seasonID: season.ID,
			scope:    "global",
			out:      seasonOut,
			pageSize: pageSize,
		}

		// Regional
		for _, reg := range regions {
			jobs <- playerLeaderboardJob{
				seasonID: season.ID,
				scope:    "regional",
				region:   reg,
				out:      seasonOut,
				pageSize: pageSize,
			}
		}

		// Realm
		for _, reg := range regions {
			for _, rslug := range realmSlugs[reg] {
				jobs <- playerLeaderboardJob{
					seasonID:  season.ID,
					scope:     "realm",
					region:    reg,
					realmSlug: rslug,
					out:       seasonOut,
					pageSize:  pageSize,
				}
			}
		}

		// Class (each handles global/regional/rea
lm internally)
		for _, cls := range classKeys {
			jobs <- playerLeaderboardJob{
				seasonID: season.ID,
				scope:    "class",
				classKey: cls,
				out:      seasonOut,
				pageSize: pageSize,
				regions:  regions,
			}
		}
	}
	close(jobs)

	wg.Wait()

	if err := firstErr.Load(); err != nil {
		return err.(error)
	}

	fmt.Printf("\n[OK] Generated %d player lead
erboards\n", completed.Load())
	return nil
}

func generateGlobalPlayerLeaderboard(db *sql
.DB, out string, pageSize int, seasonID int)
 error {
	dir := filepath.Join(out, "players", "globa
l")
	if err := os.MkdirAll(dir, 0o755); err != n
il {
		return err
	}
	rows, err := db.Query(`
		SELECT p.id, p.name, r.slug, r.name, r.reg
ion,
			   COALESCE(pd.class_name,''), COALESCE(p
d.active_spec_name,''), pp.main_spec_id,
			   pp.combined_best_time, pp.dungeons_com
pleted, pp.total_runs,
			   COALESCE(pp.global_ranking_bracket, ''
)
		FROM players p
		JOIN realms r ON p.realm_id = r.id
		JOIN player_profiles pp ON p.id = pp.playe
r_id
		LEFT JOIN player_details pd ON p.id = pd.p
layer_id
		WHERE pp.season_id = ? AND pp.has_complete
_coverage = 1 AND pp.combined_best_time IS N
OT NULL
		ORDER BY pp.combined_best_time ASC, p.name
 ASC
	`, seasonID)
	if err != nil {
		return fmt.Errorf("players query (global, 
season %d): %w", seasonID, err)
	}
	all, err := scanPlayerRows(rows)
	if err != nil {
		return err
	}
	return writePlayerLeaderboardPages(dir, all
, "Global Player Rankings", pageSize)
}

func generateRegionalPlayerLeaderboard(db *s
ql.DB, out, region string, pageSize int, sea
sonID int) error {
	dir := filepath.Join(out, "players", "regio
nal", region)
	if err := os.MkdirAll(dir, 0o755); err != n
il {
		return err
	}
	rows, err := db.Query(`
		SELECT p.id, p.name, r.slug, r.name, r.reg
ion,
			   COALESCE(pd.class_name,''), COALESCE(p
d.active_spec_name,''), pp.main_spec_id,
			   pp.combined_best_time, pp.dungeons_com
pleted, pp.total_runs,
			   COALESCE(pp.regional_ranking_bracket, 
'')
		FROM players p
		JOIN realms r ON p.realm_id = r.id
		JOIN player_profiles pp ON p.id = pp.playe
r_id
		LEFT JOIN player_details pd ON p.id = pd.p
layer_id
		WHERE pp.season_id = ? AND r.region = ? AN
D pp.has_complete_coverage = 1 AND pp.combin
ed_best_time IS NOT NULL
		ORDER BY pp.combined_best_time ASC, p.name
 ASC
	`, seasonID, region)
	if err != nil {
		return fmt.Errorf("players query (regional
, season %d): %w", seasonID, err)
	}
	all, err := scanPlayerRows(rows)
	if err != nil {
		return err
	}
	title := strings.ToUpper(region) + " Player
 Rankings"
	return writePlayerLeaderboardPages(dir, all
, title, pageSize)
}

// pool = parent + child realms (matches per
-season realm leaderboard)
func generateSingleRealmPlayerLeaderboard(db
 *sql.DB, out, region, rslug string, pageSiz
e int, seasonID int) error {
	dir := filepath.Join(out, "players", "realm
", region, rslug)
	if err := os.MkdirAll(dir, 0o755); err != n
il {
		return err
	}
	rows, err := db.Query(`
		SELECT p.id, p.name, r.slug, r.name, r.reg
ion,
			   COALESCE(pd.class_name,''), COALESCE(p
d.active_spec_name,''), pp.main_spec_id,
			   pp.combined_best_time, pp.dungeons_com
pleted, pp.total_runs,
			   COALESCE(pp.realm_ranking_bracket, '')
		FROM players p
		JOIN realms r ON p.realm_id = r.id
		JOIN player_profiles pp ON p.id = pp.playe
r_id
		LEFT JOIN player_details pd ON p.id = pd.p
layer_id
		WHERE pp.season_id = ? AND r.region = ?
			AND (r.slug = ? OR r.parent_realm_slug = 
?)
			AND pp.has_complete_coverage = 1 AND pp.c
ombined_best_time IS NOT NULL
		ORDER BY pp.combined_best_time ASC, p.name
 ASC
	`, seasonID, region, rslug, rslug)
	if err != nil {
		return fmt.Errorf("players query (realm, s
eason %d): %w", seasonID, err)
	}
	all, err := scanPlayerRows(rows)
	if err != nil {
		return err
	}
	title := strings.ToUpper(region) + "/" + rs
lug + " Player Rankings"
	return writePlayerLeaderboardPages(dir, all
, title, pageSize)
}

func writePlayerLeaderboardPages(dir string,
 all []map[string]any, title string, pageSiz
e int) error {
	total := len(all)
	pages := (total + pageSize - 1) / pageSize
	for p := 1; p <= pages; p++ {
		start := (p - 1) * pageSize
		end := start + pageSize
		if end > total {
			end = total
		}
		page := buildPlayerLeaderboardPage(all[sta
rt:end], title, total, pages, p, pageSize)
		if err := writer.WriteJSONFileCompact(file
path.Join(dir, fmt.Sprintf("%d.json", p)), p
age); err != nil {
			return err
		}
	}
	return nil
}

// generateClassPlayerLeaderboards generates
 class-filtered player rankings for a season
func generateClassPlayerLeaderboards(db *sql
.DB, out, classKey string, pageSize int, reg
ions []string, seasonID int) error {
	// Global class leaderboard
	if err := generateClassScope(db, out, "glob
al", "", "", classKey, pageSize, seasonID); 
err != nil {
		return err
	}

	// Regional class leaderboards
	for _, reg := range regions {
		if err := generateClassScope(db, out, "reg
ional", reg, "", classKey, pageSize, seasonI
D); err != nil {
			return err
		}

		// Realm class leaderboards - only for par
ent/independent realms (skip child realms)
		slugs, err := loadParentRealmSlugs(db, reg
)
		if err != nil {
			return err
		}

		for _, rslug := range slugs {
			if err := generateClassScope(db, out, "re
alm", reg, rslug, classKey, pageSize, season
ID); err != nil {
				return err
			}
		}
	}
	return nil
}

// generateClassScope generates a class-filt
ered leaderboard for a specific scope and se
ason
func generateClassScope(db *sql.DB, out, sco
pe, region, realmSlug, classKey string, page
Size int, seasonID int) error {
	var dir string
	if scope == "global" {
		dir = filepath.Join(out, "players", "class
", classKey, "global")
	} else if scope == "regional" {
		dir = filepath.Join(out, "players", "class
", classKey, "regional", region)
	} else {
		dir = filepath.Join(out, "players", "class
", classKey, "realm", region, realmSlug)
	}
	if err := os.MkdirAll(dir, 0o755); err != n
il {
		return err
	}

	// Fetch ALL players for this season, then 
filter by class in Go (to handle fallback lo
gic)
	var rows *sql.Rows
	var err error
	if scope == "global" {
		rows, err = db.Query(`
			SELECT p.id, p.name, r.slug, r.name, r.re
gion,
				   COALESCE(pd.class_name,''), COALESCE(
pd.active_spec_name,''), pp.main_spec_id,
				   pp.combined_best_time, pp.dungeons_co
mpleted, pp.total_runs,
				   COALESCE(pp.global_class_bracket, '')
			FROM players p
			JOIN realms r ON p.realm_id = r.id
			JOIN player_profiles pp ON p.id = pp.play
er_id
			LEFT JOIN player_details pd ON p.id = pd.
player_id
			WHERE pp.season_id = ? AND pp.has_complet
e_coverage = 1 AND pp.combined_best_time IS 
NOT NULL
				AND pp.class_name IS NOT NULL
			ORDER BY pp.combined_best_time ASC, p.nam
e ASC
		`, seasonID)
	} else if scope == "regional" {
		rows, err = db.Query(`
			SELECT p.id, p.name, r.slug, r.name, r.re
gion,
				   COALESCE(pd.class_name,''), COALESCE(
pd.active_spec_name,''), pp.main_spec_id,
				   pp.combined_best_time, pp.dungeons_co
mpleted, pp.total_runs,
				   COALESCE(pp.region_class_bracket, '')
			FROM players p
			JOIN realms r ON p.realm_id = r.id
			JOIN player_profiles pp ON p.id = pp.play
er_id
			LEFT JOIN player_details pd ON p.id = pd.
player_id
			WHERE pp.season_id = ? AND r.region = ? A
ND pp.has_complete_coverage = 1 AND pp.combi
ned_best_time IS NOT NULL
				AND pp.class_name IS NOT NULL
			ORDER BY pp.combined_best_time ASC, p.nam
e ASC
		`, seasonID, region)
	} else {
		// Realm scope - include entire pool (pare
nt + all children)
		rows, err = db.Query(`
			SELECT p.id, p.name, r.slug, r.name, r.re
gion,
				   COALESCE(pd.class_name,''), COALESCE(
pd.active_spec_name,''), pp.main_spec_id,
				   pp.combined_best_time, pp.dungeons_co
mpleted, pp.total_runs,
				   COALESCE(pp.realm_class_bracket, '')
			FROM players p
			JOIN realms r ON p.realm_id = r.id
			JOIN player_profiles pp ON p.id = pp.play
er_id
			LEFT JOIN player_details pd ON p.id = pd.
player_id
			WHERE pp.season_id = ? AND r.region = ?
				AND (r.slug = ? OR r.parent_realm_slug =
 ?)
				AND pp.has_complete_coverage = 1 AND pp.
combined_best_time IS NOT NULL
				AND pp.class_name IS NOT NULL
			ORDER BY pp.combined_best_time ASC, p.nam
e ASC
		`, seasonID, region, realmSlug, realmSlug)
	}
	if err != nil {
		return err
	}
	defer rows.Close()

	type PlayerRow struct {
		ID                                        
                    int64
		Name, RealmSlug, RealmName, Region, ClassN
ame, ActiveSpecName string
		MainSpecID                                
                    sql.NullInt64
		CombinedBestTime                          
                    sql.NullInt64
		DungeonsCompleted, TotalRuns              
                    int
		RankingBracket                            
                    string
	}

	// Collect all matching players (filter by 
class in Go)
	var allMatching []PlayerRow
	for rows.Next() {
		var r PlayerRow
		if err := rows.Scan(&r.ID, &r.Name, &r.Rea
lmSlug, &r.RealmName, &r.Region, &r.ClassNam
e, &r.ActiveSpecName, &r.MainSpecID, &r.Comb
inedBestTime, &r.DungeonsCompleted, &r.Total
Runs, &r.RankingBracket); err != nil {
			return err
		}

		// Derive class from main_spec_id if missi
ng
		derivedClass := r.ClassName
		derivedSpec := r.ActiveSpecName
		if (derivedClass == "" || derivedSpec == "
") && r.MainSpecID.Valid {
			if cls, spec, ok := wow.GetClassAndSpec(i
nt(r.MainSpecID.Int64)); ok {
				if derivedClass == "" {
					derivedClass = cls
				}
				if derivedSpec == "" {
					derivedSpec = spec
				}
			}
		}

		// Filter by class (case-insensitive, spac
e -> underscore)
		playerClassKey := strings.ReplaceAll(strin
gs.ToLower(derivedClass), " ", "_")
		if playerClassKey == classKey {
			r.ClassName = derivedClass
			r.ActiveSpecName = derivedSpec
			allMatching = append(allMatching, r)
		}
	}

	// Paginate the filtered results
	total := len(allMatching)
	pages := (total + pageSize - 1) / pageSize

	for p := 1; p <= pages; p++ {
		offset := (p - 1) * pageSize
		end := offset + pageSize
		if end > total {
			end = total
		}

		var list []map[string]any
		for _, r := range allMatching[offset:end] 
{
			obj := map[string]any{
				"player_id":          r.ID,
				"name":               r.Name,
				"realm_slug":         r.RealmSlug,
				"realm_name":         r.RealmName,
				"region":             r.Region,
				"class_name":         r.ClassName,
				"active_spec_name":   r.ActiveSpecName,
				"dungeons_completed": r.DungeonsComplete
d,
				"total_runs":         r.TotalRuns,
			}
			if r.RankingBracket != "" {
				obj["ranking_percentile"] = r.RankingBra
cket
			}
			if r.MainSpecID.Valid {
				obj["main_spec_id"] = int(r.MainSpecID.I
nt64)
			}
			if r.CombinedBestTime.Valid {
				obj["combined_best_time"] = r.CombinedBe
stTime.Int64
			}
			list = append(list, obj)
		}

		title := "Global Player Rankings"
		if scope == "regional" {
			title = strings.ToUpper(region) + " Playe
r Rankings"
		} else if scope == "realm" {
			title = strings.ToUpper(region) + "/" + r
ealmSlug + " Player Rankings"
		}

		page := buildPlayerLeaderboardPage(list, t
itle, total, pages, p, pageSize)
		if err := writer.WriteJSONFileCompact(file
path.Join(dir, fmt.Sprintf("%d.json", p)), p
age); err != nil {
			return err
		}
	}
	return nil
}

// scanPlayerRows scans player rows and appl
ies class/spec fallback
func scanPlayerRows(rows *sql.Rows) ([]map[s
tring]any, error) {
	defer rows.Close()

	type PlayerRow struct {
		ID                int64
		Name              string
		RealmSlug         string
		RealmName         string
		Region            string
		ClassName         string
		ActiveSpecName    string
		MainSpecID        sql.NullInt64
		CombinedBestTime  sql.NullInt64
		DungeonsCompleted int
		TotalRuns         int
		RankingBracket    string
	}

	var list []map[string]any
	for rows.Next() {
		var r PlayerRow
		if err := rows.Scan(&r.ID, &r.Name, &r.Rea
lmSlug, &r.RealmName, &r.Region, &r.ClassNam
e, &r.ActiveSpecName, &r.MainSpecID, &r.Comb
inedBestTime, &r.DungeonsCompleted, &r.Total
Runs, &r.RankingBracket); err != nil {
			return nil, err
		}

		obj := map[string]any{
			"player_id":          r.ID,
			"name":               r.Name,
			"realm_slug":         r.RealmSlug,
			"realm_name":         r.RealmName,
			"region":             r.Region,
			"class_name":         r.ClassName,
			"active_spec_name":   r.ActiveSpecName,
			"dungeons_completed": r.DungeonsCompleted
,
			"total_runs":         r.TotalRuns,
		}

		if r.RankingBracket != "" {
			obj["ranking_percentile"] = r.RankingBrac
ket
		}

		// Apply class/spec fallback if missing
		if r.MainSpecID.Valid {
			v := int(r.MainSpecID.Int64)
			obj["class_name"], obj["active_spec_name"
] = wow.FallbackClassAndSpec(r.ClassName, r.
ActiveSpecName, &v)
			obj["main_spec_id"] = v
		}

		if r.CombinedBestTime.Valid {
			obj["combined_best_time"] = r.CombinedBes
tTime.Int64
		}
		list = append(list, obj)
	}
	return list, nil
}

// buildPlayerLeaderboardPage builds a playe
r leaderboard page structure
func buildPlayerLeaderboardPage(leaderboard 
[]map[string]any, title string, total, total
Pages, currentPage, pageSize int) map[string
]any {
	return map[string]any{
		"leaderboard":         leaderboard,
		"title":               title,
		"generated_timestamp": time.Now().UnixMill
i(),
		"pagination": map[string]any{
			"currentPage":  currentPage,
			"pageSize":     pageSize,
			"totalPlayers": total,
			"totalPages":   totalPages,
			"hasNextPage":  currentPage < totalPages,
			"hasPrevPage":  currentPage > 1,
			"totalRuns":    total, // keep compatibil
ity with frontend
		},
	}
}
 
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET