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

package pipeline

import (
	"context"
	"fmt"
	"ookstats/internal/blizzard"
	"ookstats/internal/database"
	"strings"
	"time"
)

// FetchCMOptions contains options for fetching challenge mode leaderboards
type FetchCMOptions struct {
	Verbose           bool
	Regions           []string
	Realms            []string
	Dungeons          []string
	Periods           []string
	LatestPeriodsOnly bool
	Concurrency       int
	Timeout           time.Duration
}

// FetchCMResult contains statistics from the fetch operation
type FetchCMResult struct {
	TotalRuns    int
	TotalPlayers int
	Duration     time.Duration
}

// FetchChallengeMode fetches challenge mode leaderboard data for specified realms
/dungeons/periods
func FetchChallengeMode(db *database.DatabaseService, client *blizzard.Client, opt
s FetchCMOptions) (*FetchCMResult, error) {
	// Get dungeons and realms from hardcoded lists
	_, dungeons := blizzard.GetHardcodedPeriodAndDungeons()
	allRealms := blizzard.GetAllRealms()

	fmt.Printf("Dungeons: %d, Realms: %d\n", len(dungeons), len(allRealms))

	// Apply region filter
	if len(opts.Regions) > 0 {
		allowed := make(map[string]bool)
		for _, r := range opts.Regions {
			allowed[strings.TrimSpace(r)] = true
		}
		for slug, info := range allRealms {
			if !allowed[info.Region] {
				delete(allRealms, slug)
			}
		}
	}

	// Apply realm filter
	if len(opts.Realms) > 0 {
		allowed := make(map[string]bool)
		for _, s := range opts.Realms {
			s = strings.TrimSpace(s)
			if s != "" {
				allowed[s] = true
			}
		}
		filtered := make(map[string]blizzard.RealmInfo)
		for key, info := range allRealms {
			if allowed[key] || allowed[info.Slug] {
				filtered[key] = info
			}
		}
		allRealms = filtered
	}

	// Apply dungeon filter
	if len(opts.Dungeons) > 0 {
		allowed := make(map[string]bool)
		for _, s := range opts.Dungeons {
			allowed[strings.TrimSpace(s)] = true
		}
		filtered := make([]blizzard.DungeonInfo, 0, len(dungeons))
		for _, d := range dungeons {
			idStr := fmt.Sprintf("%d", d.ID)
			if allowed[idStr] || allowed[d.Slug] {
				filtered = append(filtered, d)
			}
		}
		if len(filtered) > 0 {
			dungeons = filtered
		}
	}

	// Pre-populate reference data (includes ALL realms, even children)
	fmt.Printf("Pre-populating reference data...\n")
	fmt.Printf("  - Ensuring dungeons (%d)\n", len(dungeons))
	if err := db.EnsureDungeonsOnce(dungeons); err != nil {
		return nil, fmt.Errorf("failed to ensure dungeons: %w", err)
	}
	fmt.Printf("  [OK] Dungeons ensured\n")
	fmt.Printf("  - Ensuring realms (%d, including child realms)\n", len(allRealms))
	if err := db.EnsureRealmsBatch(allRealms); err != nil {
		return nil, fmt.Errorf("failed to ensure realms: %w", err)
	}
	fmt.Printf("  [OK] Realms ensured\n")

	// Filter out child realms for fetching (they don't have their own leaderboards)
	// Players from child realms appear on parent realm leaderboards
	fetchRealms := make(map[string]blizzard.RealmInfo)
	childRealmsFiltered := 0
	for slug, info := range allRealms {
		if info.ParentRealmSlug != "" {
			childRealmsFiltered++
		} else {
			fetchRealms[slug] = info
		}
	}
	if childRealmsFiltered > 0 {
		fmt.Printf("Filtered out %d child realms from fetch (no leaderboards to fetch)\n
", childRealmsFiltered)
	}
	fmt.Printf("Reference data populated for %d realms (%d to fetch from) and %d dung
eons\n",
		len(allRealms), len(fetchRealms), len(dungeons))

	// Group realms by region (only fetch realms, excluding children)
	realmsByRegion := make(map[string]map[string]blizzard.RealmInfo)
	for slug, info := range fetchRealms {
		if realmsByRegion[info.Region] == nil {
			realmsByRegion[info.Region] = make(map[string]blizzard.RealmInfo)
		}
		realmsByRegion[info.Region][slug] = info
	}

	ctx, cancel := context.WithTimeout(context.Background(), opts.Timeout)
	defer cancel()

	totalRuns := 0
	totalPlayers := 0
	sweepStart := time.Now()

	// Process each region independently
	for region, regionRealms := range realmsByRegion {
		fmt.Printf("\n========== Region: %s (%d realms) ==========\n", strings.ToUpper(r
egion), len(regionRealms))

		// Determine periods for this region
		var periods []string

		if len(opts.Periods) > 0 {
			// User-specified periods
			periods = opts.Periods
			fmt.Printf("Using user-specified periods: %v (%d periods)\n", periods, len(peri
ods))
		} else {
			// Fetch periods from database (populated by season sync)
			var periodInts []int
			var err error

			if opts.LatestPeriodsOnly {
				fmt.Printf("Fetching latest 2 periods from current season for %s...\n", string
s.ToUpper(region))
				periodInts, err = db.GetLatestPeriodsPerRegion(region)
			} else {
				fmt.Printf("Fetching period list from database for %s...\n", strings.ToUpper(r
egion))
				periodInts, err = db.GetPeriodsForRegion(region)
			}

			if err != nil {
				fmt.Printf("Failed to fetch periods from database for %s: %v - skipping region
\n", strings.ToUpper(region), err)
				continue
			}

			// Convert []int to []string for compatibility
			periods = make([]string, len(periodInts))
			for i, p := range periodInts {
				periods[i] = fmt.Sprintf("%d", p)
			}

			if len(periods) == 0 {
				fmt.Printf("No periods found in database for %s (run season sync first) - skip
ping region\n", strings.ToUpper(region))
				continue
			}

			if opts.LatestPeriodsOnly {
				fmt.Printf("[OK] Using latest %d period(s) from current season for %s: %v\n",
					len(periods), strings.ToUpper(region), periods)
			} else {
				fmt.Printf("[OK] Fetched %d periods from database for %s (newest: %s, oldest: 
%s)\n",
					len(periods), strings.ToUpper(region), periods[0], periods[len(periods)-1])
			}
		}

		if len(periods) == 0 {
			fmt.Printf("No periods to process for %s - skipping region\n", strings.ToUpper(
region))
			continue
		}

		// Period sweep for this region
		fmt.Printf("Starting period sweep for %s: %d periods\n", strings.ToUpper(region)
, len(periods))
		for _, period := range periods {
			fmt.Printf("\n--- %s Period %s ---\n", strings.ToUpper(region), period)
			res := client.FetchAllRealmsConcurrent(ctx, regionRealms, dungeons, period)
			runs, players, berr := db.BatchProcessFetchResults(ctx, res)
			if berr != nil {
				fmt.Printf("Batch errors in %s period %s: %v\n", strings.ToUpper(region), peri
od, berr)
			}
			fmt.Printf("%s Period %s -> inserted runs: %d, new players: %d\n", strings.ToUp
per(region), period, runs, players)
			totalRuns += runs
			totalPlayers += players
		}
	}

	duration := time.Since(sweepStart)
	fmt.Printf("\n========== Sweep complete in %v ==========\n", duration)

	// Update fetch metadata
	if err := db.UpdateFetchMetadata("challenge_mode_leaderboard", totalRuns, totalPl
ayers); err != nil {
		return nil, fmt.Errorf("failed to update fetch metadata: %w", err)
	}

	return &FetchCMResult{
		TotalRuns:    totalRuns,
		TotalPlayers: totalPlayers,
		Duration:     duration,
	}, nil
}

// FetchProfilesOptions contains options for fetching player profiles
type FetchProfilesOptions struct {
	Verbose    bool
	BatchSize  int
	MaxPlayers int
	StaleAfter time.Duration
}

// FetchProfilesResult contains statistics from the profile fetch operation
type FetchProfilesResult struct {
	TotalProfiles  int
	TotalEquipment int
	ProcessedCount int
	Duration       time.Duration
}

// FetchPlayerProfiles fetches detailed player profile data including equipment
func FetchPlayerProfiles(db *database.DatabaseService, client *blizzard.Client, op
ts FetchProfilesOptions) (*FetchProfilesResult, error) {
	// Compute staleness cutoff
	staleCutoff := int64(0)
	if opts.StaleAfter > 0 {
		staleCutoff = time.Now().Add(-opts.StaleAfter).UnixMilli()
	}

	// Get eligible players (9/9 completion)
	fmt.Println("Finding eligible players with complete coverage (9/9 dungeons)...")
	players, err := db.GetEligiblePlayersForProfileFetch(staleCutoff)
	if err != nil {
		return nil, fmt.Errorf("failed to get eligible players: %w", err)
	}

	if len(players) == 0 {
		fmt.Println("No eligible players found. Run 'ookstats process players' first to 
generate player profiles.")
		return &FetchProfilesResult{}, nil
	}

	fmt.Printf("Found %d eligible players with 9/9 completion\n", len(players))

	// Apply max players limit
	if opts.MaxPlayers > 0 && len(players) > opts.MaxPlayers {
		players = players[:opts.MaxPlayers]
		fmt.Printf("Limited to first %d players due to max-players limit\n", opts.MaxPla
yers)
	}

	// Default batch size
	batchSize := opts.BatchSize
	if batchSize <= 0 {
		batchSize = 20
	}

	fmt.Printf("Processing %d players in batches of %d with 20 concurrent requests\n"
, len(players), batchSize)
	fmt.Printf("\nStarting player profile fetching...\n")

	startTime := time.Now()
	totalProfiles := 0
	totalEquipment := 0
	processedCount := 0

	// Process in batches to avoid overwhelming the API
	for i := 0; i < len(players); i += batchSize {
		end := i + batchSize
		if end > len(players) {
			end = len(players)
		}
		batch := players[i:end]

		batchNumber := (i / batchSize) + 1
		totalBatches := (len(players) + batchSize - 1) / batchSize

		fmt.Printf("\n--- Batch %d/%d (%d players) ---\n", batchNumber, totalBatches, le
n(batch))

		// Fetch profiles concurrently for this batch with fallback attempts
		sem := make(chan struct{}, 20)
		type out struct {
			profiles  int
			equipment int
			err       error
		}
		outCh := make(chan out, len(batch))
		timestamp := time.Now().UnixMilli()

		for _, p := range batch {
			sem <- struct{}{}
			go func(p blizzard.PlayerInfo) {
				defer func() { <-sem }()

				// Build candidate list
				region := p.Region
				realm := blizzard.NormalizeRealmSlug(region, p.RealmSlug)
				name := p.Name
				tried := map[string]bool{}
				candidates := [][2]string{{realm, name}}

				// Connected-realm sweep
				if slugs, err := db.GetConnectedRealmSlugs(region, realm); err == nil {
					for _, s := range slugs {
						s = blizzard.NormalizeRealmSlug(region, s)
						if s != realm {
							candidates = append(candidates, [2]string{s, name})
						}
					}
				}

				// Last-run realm heuristic
				if lrRegion, lrSlug, _, err := db.GetLastRunRealmForPlayer(p.ID); err == nil &
& lrRegion != "" && lrSlug != "" {
					if lrRegion == region {
						lrSlug = blizzard.NormalizeRealmSlug(region, lrSlug)
						candidates = append(candidates, [2]string{lrSlug, name})
					}
				}

				// Attempt candidates
				var profs, items int
				var finalErr error
				for _, c := range candidates {
					key := c[0] + "|" + c[1]
					if tried[key] {
						continue
					}
					tried[key] = true

					// Fetch summary first; if it 404s, skip to next candidate
					sum, err := client.FetchCharacterSummary(c[1], c[0], region)
					if err != nil {
						// try next candidate
						finalErr = err
						continue
					}
					eq, err2 := client.FetchCharacterEquipment(c[1], c[0], region)
					if err2 != nil {
						finalErr = err2
					}
					med, err3 := client.FetchCharacterMedia(c[1], c[0], region)
					if err3 != nil {
						finalErr = err3
					}

					// Insert profile data
					res := blizzard.PlayerProfileResult{
						PlayerID:   p.ID,
						PlayerName: c[1],
						RealmSlug:  c[0],
						Region:     region,
						Summary:    sum,
						Equipment:  eq,
						Media:      med,
					}
					pr, eqc, derr := db.InsertPlayerProfileData(res, timestamp)
					if derr != nil {
						finalErr = derr
						continue
					}
					profs += pr
					items += eqc
					// Update players table to resolved identity for this build
					_ = db.UpdatePlayerIdentity(p.ID, c[1], region, c[0])
					break
				}
				outCh <- out{profiles: profs, equipment: items, err: finalErr}
			}(p)
		}

		// Wait for batch: collect exactly len(batch) results
		batchProfiles := 0
		batchEquipment := 0
		for k := 0; k < len(batch); k++ {
			r := <-outCh
			processedCount++
			if r.err != nil && r.profiles == 0 && r.equipment == 0 {
				// only log errors if nothing was inserted
				if opts.Verbose {
					fmt.Printf("  [ERROR] profile fetch failed: %v\n", r.err)
				}
			}
			batchProfiles += r.profiles
			batchEquipment += r.equipment
		}
		close(outCh)

		totalProfiles += batchProfiles
		totalEquipment += batchEquipment

		elapsed := time.Since(startTime)
		fmt.Printf("  -> Batch %d complete: %d profiles, %d items (Total: %d/%d players,
 %.1f players/min)\n",
			batchNumber, batchProfiles, batchEquipment, processedCount, len(players),
			float64(processedCount)/elapsed.Minutes())

		// Small delay between batches to be respectful to the API
		if i+batchSize < len(players) {
			fmt.Printf("  [INFO] Waiting 1 second before next batch...\n")
			time.Sleep(1 * time.Second)
		}
	}

	elapsed := time.Since(startTime)
	return &FetchProfilesResult{
		TotalProfiles:  totalProfiles,
		TotalEquipment: totalEquipment,
		ProcessedCount: processedCount,
		Duration:       elapsed,
	}, nil
}

package pipeline

import (
	"context"
	"fmt"
	"ookstats/internal/blizzard"
	"ookstats/internal/database"
	"strings"
	"time"
)

// FetchCMOptions contains options for fetch
ing challenge mode leaderboards
type FetchCMOptions struct {
	Verbose           bool
	Regions           []string
	Realms            []string
	Dungeons          []string
	Periods           []string
	LatestPeriodsOnly bool
	Concurrency       int
	Timeout           time.Duration
}

// FetchCMResult contains statistics from th
e fetch operation
type FetchCMResult struct {
	TotalRuns    int
	TotalPlayers int
	Duration     time.Duration
}

// FetchChallengeMode fetches challenge mode
 leaderboard data for specified realms/dunge
ons/periods
func FetchChallengeMode(db *database.Databas
eService, client *blizzard.Client, opts Fetc
hCMOptions) (*FetchCMResult, error) {
	// Get dungeons and realms from hardcoded l
ists
	_, dungeons := blizzard.GetHardcodedPeriodA
ndDungeons()
	allRealms := blizzard.GetAllRealms()

	fmt.Printf("Dungeons: %d, Realms: %d\n", le
n(dungeons), len(allRealms))

	// Apply region filter
	if len(opts.Regions) > 0 {
		allowed := make(map[string]bool)
		for _, r := range opts.Regions {
			allowed[strings.TrimSpace(r)] = true
		}
		for slug, info := range allRealms {
			if !allowed[info.Region] {
				delete(allRealms, slug)
			}
		}
	}

	// Apply realm filter
	if len(opts.Realms) > 0 {
		allowed := make(map[string]bool)
		for _, s := range opts.Realms {
			s = strings.TrimSpace(s)
			if s != "" {
				allowed[s] = true
			}
		}
		filtered := make(map[string]blizzard.Realm
Info)
		for key, info := range allRealms {
			if allowed[key] || allowed[info.Slug] {
				filtered[key] = info
			}
		}
		allRealms = filtered
	}

	// Apply dungeon filter
	if len(opts.Dungeons) > 0 {
		allowed := make(map[string]bool)
		for _, s := range opts.Dungeons {
			allowed[strings.TrimSpace(s)] = true
		}
		filtered := make([]blizzard.DungeonInfo, 0
, len(dungeons))
		for _, d := range dungeons {
			idStr := fmt.Sprintf("%d", d.ID)
			if allowed[idStr] || allowed[d.Slug] {
				filtered = append(filtered, d)
			}
		}
		if len(filtered) > 0 {
			dungeons = filtered
		}
	}

	// Pre-populate reference data (includes AL
L realms, even children)
	fmt.Printf("Pre-populating reference data..
.\n")
	fmt.Printf("  - Ensuring dungeons (%d)\n", 
len(dungeons))
	if err := db.EnsureDungeonsOnce(dungeons); 
err != nil {
		return nil, fmt.Errorf("failed to ensure d
ungeons: %w", err)
	}
	fmt.Printf("  [OK] Dungeons ensured\n")
	fmt.Printf("  - Ensuring realms (%d, includ
ing child realms)\n", len(allRealms))
	if err := db.EnsureRealmsBatch(allRealms); 
err != nil {
		return nil, fmt.Errorf("failed to ensure r
ealms: %w", err)
	}
	fmt.Printf("  [OK] Realms ensured\n")

	// Filter out child realms for fetching (th
ey don't have their own leaderboards)
	// Players from child realms appear on pare
nt realm leaderboards
	fetchRealms := make(map[string]blizzard.Rea
lmInfo)
	childRealmsFiltered := 0
	for slug, info := range allRealms {
		if info.ParentRealmSlug != "" {
			childRealmsFiltered++
		} else {
			fetchRealms[slug] = info
		}
	}
	if childRealmsFiltered > 0 {
		fmt.Printf("Filtered out %d child realms f
rom fetch (no leaderboards to fetch)\n", chi
ldRealmsFiltered)
	}
	fmt.Printf("Reference data populated for %d
 realms (%d to fetch from) and %d dungeons\n
",
		len(allRealms), len(fetchRealms), len(dung
eons))

	// Group realms by region (only fetch realm
s, excluding children)
	realmsByRegion := make(map[string]map[strin
g]blizzard.RealmInfo)
	for slug, info := range fetchRealms {
		if realmsByRegion[info.Region] == nil {
			realmsByRegion[info.Region] = make(map[st
ring]blizzard.RealmInfo)
		}
		realmsByRegion[info.Region][slug] = info
	}

	ctx, cancel := context.WithTimeout(context.
Background(), opts.Timeout)
	defer cancel()

	totalRuns := 0
	totalPlayers := 0
	sweepStart := time.Now()

	// Process each region independently
	for region, regionRealms := range realmsByR
egion {
		fmt.Printf("\n========== Region: %s (%d re
alms) ==========\n", strings.ToUpper(region)
, len(regionRealms))

		// Determine periods for this region
		var periods []string

		if len(opts.Periods) > 0 {
			// User-specified periods
			periods = opts.Periods
			fmt.Printf("Using user-specified periods:
 %v (%d periods)\n", periods, len(periods))
		} else {
			// Fetch periods from database (populated
 by season sync)
			var periodInts []int
			var err error

			if opts.LatestPeriodsOnly {
				fmt.Printf("Fetching latest 2 periods fr
om current season for %s...\n", strings.ToUp
per(region))
				periodInts, err = db.GetLatestPeriodsPer
Region(region)
			} else {
				fmt.Printf("Fetching period list from da
tabase for %s...\n", strings.ToUpper(region)
)
				periodInts, err = db.GetPeriodsForRegion
(region)
			}

			if err != nil {
				fmt.Printf("Failed to fetch periods from
 database for %s: %v - skipping region\n", s
trings.ToUpper(region), err)
				continue
			}

			// Convert []int to []string for compatib
ility
			periods = make([]string, len(periodInts))
			for i, p := range periodInts {
				periods[i] = fmt.Sprintf("%d", p)
			}

			if len(periods) == 0 {
				fmt.Printf("No periods found in database
 for %s (run season sync first) - skipping r
egion\n", strings.ToUpper(region))
				continue
			}

			if opts.LatestPeriodsOnly {
				fmt.Printf("[OK] Using latest %d period(
s) from current season for %s: %v\n",
					len(periods), strings.ToUpper(region), 
periods)
			} else {
				fmt.Printf("[OK] Fetched %d periods from
 database for %s (newest: %s, oldest: %s)\n"
,
					len(periods), strings.ToUpper(region), 
periods[0], periods[len(periods)-1])
			}
		}

		if len(periods) == 0 {
			fmt.Printf("No periods to process for %s 
- skipping region\n", strings.ToUpper(region
))
			continue
		}

		// Period sweep for this region
		fmt.Printf("Starting period sweep for %s: 
%d periods\n", strings.ToUpper(region), len(
periods))
		for _, period := range periods {
			fmt.Printf("\n--- %s Period %s ---\n", st
rings.ToUpper(region), period)
			res := client.FetchAllRealmsConcurrent(ct
x, regionRealms, dungeons, period)
			runs, players, berr := db.BatchProcessFet
chResults(ctx, res)
			if berr != nil {
				fmt.Printf("Batch errors in %s period %s
: %v\n", strings.ToUpper(region), period, be
rr)
			}
			fmt.Printf("%s Period %s -> inserted runs
: %d, new players: %d\n", strings.ToUpper(re
gion), period, runs, players)
			totalRuns += runs
			totalPlayers += players
		}
	}

	duration := time.Since(sweepStart)
	fmt.Printf("\n========== Sweep complete in 
%v ==========\n", duration)

	// Update fetch metadata
	if err := db.UpdateFetchMetadata("challenge
_mode_leaderboard", totalRuns, totalPlayers)
; err != nil {
		return nil, fmt.Errorf("failed to update f
etch metadata: %w", err)
	}

	return &FetchCMResult{
		TotalRuns:    totalRuns,
		TotalPlayers: totalPlayers,
		Duration:     duration,
	}, nil
}

// FetchProfilesOptions contains options for
 fetching player profiles
type FetchProfilesOptions struct {
	Verbose    bool
	BatchSize  int
	MaxPlayers int
	StaleAfter time.Duration
}

// FetchProfilesResult contains statistics f
rom the profile fetch operation
type FetchProfilesResult struct {
	TotalProfiles  int
	TotalEquipment int
	ProcessedCount int
	Duration       time.Duration
}

// FetchPlayerProfiles fetches detailed play
er profile data including equipment
func FetchPlayerProfiles(db *database.Databa
seService, client *blizzard.Client, opts Fet
chProfilesOptions) (*FetchProfilesResult, er
ror) {
	// Compute staleness cutoff
	staleCutoff := int64(0)
	if opts.StaleAfter > 0 {
		staleCutoff = time.Now().Add(-opts.StaleAf
ter).UnixMilli()
	}

	// Get eligible players (9/9 completion)
	fmt.Println("Finding eligible players with 
complete coverage (9/9 dungeons)...")
	players, err := db.GetEligiblePlayersForPro
fileFetch(staleCutoff)
	if err != nil {
		return nil, fmt.Errorf("failed to get elig
ible players: %w", err)
	}

	if len(players) == 0 {
		fmt.Println("No eligible players found. Ru
n 'ookstats process players' first to genera
te player profiles.")
		return &FetchProfilesResult{}, nil
	}

	fmt.Printf("Found %d eligible players with 
9/9 completion\n", len(players))

	// Apply max players limit
	if opts.MaxPlayers > 0 && len(players) > op
ts.MaxPlayers {
		players = players[:opts.MaxPlayers]
		fmt.Printf("Limited to first %d players du
e to max-players limit\n", opts.MaxPlayers)
	}

	// Default batch size
	batchSize := opts.BatchSize
	if batchSize <= 0 {
		batchSize = 20
	}

	fmt.Printf("Processing %d players in batche
s of %d with 20 concurrent requests\n", len(
players), batchSize)
	fmt.Printf("\nStarting player profile fetch
ing...\n")

	startTime := time.Now()
	totalProfiles := 0
	totalEquipment := 0
	processedCount := 0

	// Process in batches to avoid overwhelming
 the API
	for i := 0; i < len(players); i += batchSiz
e {
		end := i + batchSize
		if end > len(players) {
			end = len(players)
		}
		batch := players[i:end]

		batchNumber := (i / batchSize) + 1
		totalBatches := (len(players) + batchSize 
- 1) / batchSize

		fmt.Printf("\n--- Batch %d/%d (%d players)
 ---\n", batchNumber, totalBatches, len(batc
h))

		// Fetch profiles concurrently for this ba
tch with fallback attempts
		sem := make(chan struct{}, 20)
		type out struct {
			profiles  int
			equipment int
			err       error
		}
		outCh := make(chan out, len(batch))
		timestamp := time.Now().UnixMilli()

		for _, p := range batch {
			sem <- struct{}{}
			go func(p blizzard.PlayerInfo) {
				defer func() { <-sem }()

				// Build candidate list
				region := p.Region
				realm := blizzard.NormalizeRealmSlug(reg
ion, p.RealmSlug)
				name := p.Name
				tried := map[string]bool{}
				candidates := [][2]string{{realm, name}}

				// Connected-realm sweep
				if slugs, err := db.GetConnectedRealmSlu
gs(region, realm); err == nil {
					for _, s := range slugs {
						s = blizzard.NormalizeRealmSlug(region
, s)
						if s != realm {
							candidates = append(candidates, [2]st
ring{s, name})
						}
					}
				}

				// Last-run realm heuristic
				if lrRegion, lrSlug, _, err := db.GetLas
tRunRealmForPlayer(p.ID); err == nil && lrRe
gion != "" && lrSlug != "" {
					if lrRegion == region {
						lrSlug = blizzard.NormalizeRealmSlug(r
egion, lrSlug)
						candidates = append(candidates, [2]str
ing{lrSlug, name})
					}
				}

				// Attempt candidates
				var profs, items int
				var finalErr error
				for _, c := range candidates {
					key := c[0] + "|" + c[1]
					if tried[key] {
						continue
					}
					tried[key] = true

					// Fetch summary first; if it 404s, ski
p to next candidate
					sum, err := client.FetchCharacterSummar
y(c[1], c[0], region)
					if err != nil {
						// try next candidate
						finalErr = err
						continue
					}
					eq, err2 := client.FetchCharacterEquipm
ent(c[1], c[0], region)
					if err2 != nil {
						finalErr = err2
					}
					med, err3 := client.FetchCharacterMedia
(c[1], c[0], region)
					if err3 != nil {
						finalErr = err3
					}

					// Insert profile data
					res := blizzard.PlayerProfileResult{
						PlayerID:   p.ID,
						PlayerName: c[1],
						RealmSlug:  c[0],
						Region:     region,
						Summary:    sum,
						Equipment:  eq,
						Media:      med,
					}
					pr, eqc, derr := db.InsertPlayerProfile
Data(res, timestamp)
					if derr != nil {
						finalErr = derr
						continue
					}
					profs += pr
					items += eqc
					// Update players table to resolved ide
ntity for this build
					_ = db.UpdatePlayerIdentity(p.ID, c[1],
 region, c[0])
					break
				}
				outCh <- out{profiles: profs, equipment:
 items, err: finalErr}
			}(p)
		}

		// Wait for batch: collect exactly len(bat
ch) results
		batchProfiles := 0
		batchEquipment := 0
		for k := 0; k < len(batch); k++ {
			r := <-outCh
			processedCount++
			if r.err != nil && r.profiles == 0 && r.e
quipment == 0 {
				// only log errors if nothing was insert
ed
				if opts.Verbose {
					fmt.Printf("  [ERROR] profile fetch fai
led: %v\n", r.err)
				}
			}
			batchProfiles += r.profiles
			batchEquipment += r.equipment
		}
		close(outCh)

		totalProfiles += batchProfiles
		totalEquipment += batchEquipment

		elapsed := time.Since(startTime)
		fmt.Printf("  -> Batch %d complete: %d pro
files, %d items (Total: %d/%d players, %.1f 
players/min)\n",
			batchNumber, batchProfiles, batchEquipmen
t, processedCount, len(players),
			float64(processedCount)/elapsed.Minutes()
)

		// Small delay between batches to be respe
ctful to the API
		if i+batchSize < len(players) {
			fmt.Printf("  [INFO] Waiting 1 second bef
ore next batch...\n")
			time.Sleep(1 * time.Second)
		}
	}

	elapsed := time.Since(startTime)
	return &FetchProfilesResult{
		TotalProfiles:  totalProfiles,
		TotalEquipment: totalEquipment,
		ProcessedCount: processedCount,
		Duration:       elapsed,
	}, nil
}
 
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET