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

// Common utility functions
import { DUNGEON_MAP, dungeonNameToSlug } from "./wow-constants.js";

// Time formatting utilities
export function formatDuration(seconds: number): string {
  const minutes = Math.floor(seconds / 60);
  const remainingSeconds = seconds % 60;
  return minutes > 0
    ? `${minutes}m ${remainingSeconds}s`
    : `${remainingSeconds}s`;
}

export function formatDurationMMSS(milliseconds: number): string {
  return `${Math.floor(milliseconds / 60000)}:${String(Math.floor((milliseconds % 
60000) / 1000)).padStart(2, "0")}`;
}

// Alias for clarity when working with milliseconds
export const formatDurationFromMs = formatDurationMMSS;

export function formatTimestamp(timestamp: number): string {
  return new Date(timestamp).toLocaleDateString("en-US", {
    month: "short",
    day: "numeric",
    hour: "2-digit",
    minute: "2-digit",
  });
}

// compact "Mar 27"; formatTimestamp's time-of-day eats space in dense layouts
export function formatShortDate(timestamp: number): string {
  return new Date(timestamp).toLocaleDateString("en-US", {
    month: "short",
    day: "numeric",
  });
}

export function formatSimulationDate(timestamp: string | number): string {
  return new Date(timestamp).toLocaleString("en-US", {
    year: "numeric",
    month: "long",
    day: "numeric",
    hour: "2-digit",
    minute: "2-digit",
    timeZoneName: "short",
  });
}

export function formatRaidBuffs(
  buffs: Record<string, boolean | number>,
): string {
  const MINOR_WORDS = new Set([
    "of",
    "the",
    "and",
    "to",
    "for",
    "a",
    "an",
    "in",
    "on",
    "with",
    "vs",
  ]);

  const normalize = (key: string): string => {
    if (!key) return "";
    let s = String(key);
    // Drop trailing Count (e.g., skullBannerCount -> skullBanner)
    s = s.replace(/Count$/i, "");
    // Replace underscores with spaces and split camelCase
    s = s.replace(/_/g, " ");
    s = s.replace(/([a-z])([A-Z])/g, "$1 $2");
    // Collapse whitespace
    s = s.replace(/\s+/g, " ").trim().toLowerCase();
    // Title-case while keeping minor words lowercase
    const words = s.split(" ");
    const out = words.map((w, i) => {
      if (i > 0 && MINOR_WORDS.has(w)) return w;
      return w.charAt(0).toUpperCase() + w.slice(1);
    });
    return out.join(" ");
  };

  const activeBuffs = Object.entries(buffs)
    .filter(([_, value]) => value !== false && value !== 0)
    .map(([key, value]) => {
      const name = normalize(key);
      return typeof value === "number" && value > 1
        ? `${name} (${value})`
        : name;
    });
  return activeBuffs.join(", ");
}

export function formatRace(race: string): string {
  if (!race) return "Unknown";
  let s = String(race).trim();
  // Strip common enum prefixes like "RaceOrc" -> "Orc"
  if (/^Race[A-Z]/.test(s)) s = s.replace(/^Race/, "");
  // Replace underscores with spaces and split CamelCase into words
  s = s.replace(/_/g, " ");
  s = s.replace(/([a-z])([A-Z])/g, "$1 $2");
  // Normalize extra whitespace
  s = s.replace(/\s+/g, " ").trim();
  // Title case each word
  s = s.toLowerCase().replace(/\b\w/g, (l) => l.toUpperCase());
  return s;
}

// General string helpers
export function toTitleCase(input: string): string {
  if (!input) return "";
  const s = String(input).replace(/[_-]+/g, " ").trim().toLowerCase();
  return s.replace(/\b\w/g, (l) => l.toUpperCase());
}

export function slugifyLabel(input: string): string {
  return String(input || "")
    .trim()
    .toLowerCase()
    .replace(/\s+/g, "_");
}

// DOM utilities
export function createElement(
  tag: string,
  className?: string,
  textContent?: string,
): HTMLElement {
  const element = document.createElement(tag);
  if (className) element.className = className;
  if (textContent) element.textContent = textContent;
  return element;
}

export function toggleElementVisibility(
  element: HTMLElement,
  show: boolean,
): void {
  element.style.display = show ? "block" : "none";
}

// URL utilities
export function updateBrowserURL(url: string): void {
  window.history.pushState({}, "", url);
}

export function getURLSearchParams(): URLSearchParams {
  return new URLSearchParams(window.location.search);
}

export function buildLeaderboardURL(
  region: string,
  realm: string,
  dungeonSlug: string,
  page?: number,
  opts?: { season?: number; basePath?: string },
): string {
  const season = Number(opts?.season ?? 1);
  const basePath = opts?.basePath ?? "challenge-mode";
  const seasonSegment = season > 0 ? `season${season}` : "season1";

  const normalizedRegion = region || "global";
  const normalizedRealm = realm && realm !== "" ? realm : "all";

  let baseURL: string;

  if (normalizedRegion === "global") {
    baseURL = `/${basePath}/${seasonSegment}/global/${dungeonSlug}`;
  } else {
    const realmSlug = normalizedRealm || "all";
    baseURL = `/${basePath}/${seasonSegment}/${normalizedRegion}/${realmSlug}/${du
ngeonSlug}`;
  }

  if (page && page > 1) {
    baseURL += `?page=${page}`;
  }

  return baseURL;
}

export function buildPlayerProfileURL(
  region: string,
  realmSlug: string,
  playerName: string,
): string {
  return `/player/${region}/${realmSlug}/${playerName.toLowerCase()}`;
}

// Build Players tab URL path from filters
export function buildPlayersLeaderboardURL(
  scope: "global" | "regional" | "realm",
  opts?: {
    region?: string;
    realmSlug?: string;
    classKey?: string;
    page?: number;
  },
): string {
  const cls = (opts?.classKey || "").toLowerCase();
  const reg = (opts?.region || "").toLowerCase();
  const realm = (opts?.realmSlug || "").toLowerCase();
  let path = "/challenge-mode/players";
  if (scope === "global") {
    if (cls) path += `/global/${cls}`;
    else path += `/global`;
  } else if (scope === "regional") {
    if (reg) path += `/${reg}`;
    else path += `/us`;
    if (cls) path += `/${cls}`;
  } else if (scope === "realm") {
    if (reg) path += `/${reg}`;
    if (realm) path += `/${realm}`;
    if (cls) path += `/${cls}`;
  }
  const page = opts?.page || 1;
  if (page > 1) path += `?page=${page}`;
  return path;
}

// Form utilities
export function getFormValue(elementId: string): string {
  const element = document.getElementById(elementId) as
    | HTMLInputElement
    | HTMLSelectElement;
  return element?.value || "";
}

export function setFormValue(elementId: string, value: string): void {
  const element = document.getElementById(elementId) as
    | HTMLInputElement
    | HTMLSelectElement;
  if (element) element.value = value;
}

// Validation utilities
export function validateRequiredFields(
  ...values: (string | undefined)[]
): boolean {
  return values.every((value) => value && value.trim().length > 0);
}

// Error handling utilities
export function handleAPIError(error: Error, context: string): void {
  console.error(`${context} error:`, error);

  // Could extend this to show user-friendly error messages
  const errorMessage = error.message.includes("404")
    ? "Data not found"
    : error.message.includes("500")
      ? "Server error - please try again later"
      : "An error occurred loading data";

  console.warn(`User-friendly error: ${errorMessage}`);
}

// (Removed legacy loading state helpers in favor of <LoadingState /> component)

// Debouncing utility for search inputs
export function debounce<T extends (...args: any[]) => void>(
  func: T,
  wait: number,
): (...args: Parameters<T>) => void {
  let timeout: NodeJS.Timeout;

  return (...args: Parameters<T>) => {
    clearTimeout(timeout);
    timeout = setTimeout(() => func(...args), wait);
  };
}

// Local storage utilities
export function saveToLocalStorage(key: string, data: any): void {
  try {
    localStorage.setItem(key, JSON.stringify(data));
  } catch (error) {
    console.warn("Failed to save to localStorage:", error);
  }
}

export function loadFromLocalStorage<T>(key: string): T | null {
  try {
    const item = localStorage.getItem(key);
    return item ? JSON.parse(item) : null;
  } catch (error) {
    console.warn("Failed to load from localStorage:", error);
    return null;
  }
}

// Array utilities
export function chunk<T>(array: T[], size: number): T[][] {
  const chunks: T[][] = [];
  for (let i = 0; i < array.length; i += size) {
    chunks.push(array.slice(i, i + size));
  }
  return chunks;
}

export function unique<T>(array: T[]): T[] {
  return Array.from(new Set(array));
}

// Object utilities
export function isEmpty(obj: any): boolean {
  if (!obj) return true;
  if (Array.isArray(obj)) return obj.length === 0;
  if (typeof obj === "object") return Object.keys(obj).length === 0;
  return false;
}

// Static API utilities for file-based endpoints
export function dungeonIdToSlug(dungeonId: number): string {
  const dungeonName = DUNGEON_MAP[dungeonId as keyof typeof DUNGEON_MAP];
  if (!dungeonName) {
    throw new Error(`Unknown dungeon ID: ${dungeonId}`);
  }
  return dungeonNameToSlug(dungeonName);
}

// Build static API file paths
export function buildStaticLeaderboardPath(
  region: string,
  realm: string,
  dungeonId: number,
  page: number = 1,
  seasonId: number = 1,
): string {
  const dungeonSlug = dungeonIdToSlug(dungeonId);

  if (region === "global") {
    return `/api/leaderboard/season/${seasonId}/global/${dungeonSlug}/${page}.json
`;
  } else if (realm === "all") {
    return `/api/leaderboard/season/${seasonId}/${region}/all/${dungeonSlug}/${pag
e}.json`;
  } else {
    return `/api/leaderboard/season/${seasonId}/${region}/${realm}/${dungeonSlug}/
${page}.json`;
  }
}

export function buildStaticPlayerLeaderboardPath(
  scope: "global" | "regional" | "realm",
  region?: string,
  page: number = 1,
  opts?: { realmSlug?: string; classKey?: string; seasonId?: number },
): string {
  const cls = (opts?.classKey || "").toLowerCase();
  const realm = (opts?.realmSlug || "").toLowerCase();
  const seasonId = opts?.seasonId ?? 1;

  // Class-filtered variants
  if (cls) {
    if (scope === "global") {
      return `/api/leaderboard/season/${seasonId}/players/class/${cls}/global/${pa
ge}.json`;
    }
    if (scope === "regional" && region) {
      return `/api/leaderboard/season/${seasonId}/players/class/${cls}/regional/${
region}/${page}.json`;
    }
    if (scope === "realm" && region && realm) {
      return `/api/leaderboard/season/${seasonId}/players/class/${cls}/realm/${reg
ion}/${realm}/${page}.json`;
    }
  }

  // Unfiltered
  if (scope === "global") {
    return `/api/leaderboard/season/${seasonId}/players/global/${page}.json`;
  }
  if (scope === "regional" && region) {
    return `/api/leaderboard/season/${seasonId}/players/regional/${region}/${page}
.json`;
  }
  if (scope === "realm" && region && realm) {
    return `/api/leaderboard/season/${seasonId}/players/realm/${region}/${realm}/$
{page}.json`;
  }
  // Fallback to global
  return `/api/leaderboard/season/${seasonId}/players/global/${page}.json`;
}

// Unified leaderboard URL builder for the merged Players/Characters page.
// Picks the correct static JSON path for any (view, sort, season, scope)
// combination. Existing trees are reused when possible; new combinations
// (all-time time, per-season runs) hit the Phase 1 generators.
//
// Notes on URL drift between trees:
//   * Per-char per-season time and per-account per-season time still live at
//     /season/{N}/players/ and /season/{N}/accounts/ - the migration to
//     /season{N}/{view}/by-{sort}/ happens in Phase 3.
//   * Per-char and per-account all-time runs still live at
//     /players/total-runs/ - same reason.
type LeaderboardView = "player" | "character";
type LeaderboardSort = "time" | "runs";

export function buildUnifiedLeaderboardPath(
  view: LeaderboardView,
  sort: LeaderboardSort,
  season: number | "all-time",
  scope: "global" | "regional" | "realm" | "class",
  opts?: {
    region?: string;
    realmSlug?: string;
    classKey?: string;
    page?: number;
  },
): string {
  const page = opts?.page ?? 1;
  const region = (opts?.region || "").toLowerCase();
  const realm = (opts?.realmSlug || "").toLowerCase();
  const cls = (opts?.classKey || "").toLowerCase();

  // Build the scope segments common to most paths.
  const scopeSegments = (() => {
    if (scope === "global") return "global";
    if (scope === "regional") return `regional/${region}`;
    if (scope === "realm") return `realm/${region}/${realm}`;
    if (scope === "class") {
      // Class scope is followed by an inner geo (only valid for character view).
      // Caller signals which geo via region/realm being set: realm > region > glo
bal.
      const inner = realm
        ? `realm/${region}/${realm}`
        : region
          ? `regional/${region}`
          : "global";
      return `class/${cls}/${inner}`;
    }
    return "global";
  })();

  // All-Time time ? Phase 1 unified tree.
  if (season === "all-time" && sort === "time") {
    const v = view === "player" ? "players" : "characters";
    return `/api/leaderboard/all-time/${v}/by-time/${scopeSegments}/${page}.json`;
  }
  // All-Time runs ? existing cross-season Total Runs trees.
  if (season === "all-time" && sort === "runs") {
    if (view === "player") {
      // Account total runs (no class scope - accounts span classes)
      if (scope === "realm")
        return `/api/leaderboard/players/total-runs/accounts/realm/${region}/${rea
lm}/${page}.json`;
      if (scope === "regional")
        return `/api/leaderboard/players/total-runs/accounts/regional/${region}/${
page}.json`;
      return `/api/leaderboard/players/total-runs/accounts/global/${page}.json`;
    }
    // Per-char total runs
    if (scope === "class")
      return buildStaticTotalRunsLeaderboardPath(
        realm ? "realm" : region ? "regional" : "global",
        region,
        page,
        { realmSlug: realm || undefined, classKey: cls },
      );
    if (scope === "realm")
      return `/api/leaderboard/players/total-runs/realm/${region}/${realm}/${page}
.json`;
    if (scope === "regional")
      return `/api/leaderboard/players/total-runs/regional/${region}/${page}.json`
;
    return `/api/leaderboard/players/total-runs/global/${page}.json`;
  }
  // Per-season time + per-season runs.
  // Note season=all-time doesn't reach here.
  const seasonNum = season as number;
  if (sort === "time") {
    // Existing trees (Phase 3 will migrate them under /season{N}/.../by-time/).
    if (view === "player") {
      if (scope === "realm")
        return `/api/leaderboard/season/${seasonNum}/accounts/realm/${region}/${re
alm}/${page}.json`;
      if (scope === "regional")
        return `/api/leaderboard/season/${seasonNum}/accounts/regional/${region}/$
{page}.json`;
      return `/api/leaderboard/season/${seasonNum}/accounts/global/${page}.json`;
    }
    // character view, time, per-season ? existing per-char per-season tree
    return buildStaticPlayerLeaderboardPath(
      scope === "global"
        ? "global"
        : scope === "regional"
          ? "regional"
          : scope === "realm"
            ? "realm"
            : "global",
      region,
      page,
      {
        realmSlug: realm || undefined,
        classKey: cls || undefined,
        seasonId: seasonNum,
      },
    );
  }
  // sort === "runs" + per-season ? Phase 1 new tree.
  const v = view === "player" ? "players" : "characters";
  return `/api/leaderboard/season${seasonNum}/${v}/by-runs/${scopeSegments}/${page
}.json`;
}

// Account-grouped cross-season Total Runs (counterpart to the per-character
// total runs leaderboard, used by the Player toggle on /players/total-runs/).
export function buildStaticAccountTotalRunsPath(
  scope: "global" | "regional" | "realm",
  region?: string,
  page: number = 1,
  opts?: { realmSlug?: string },
): string {
  const realm = (opts?.realmSlug || "").toLowerCase();
  if (scope === "realm" && region && realm) {
    return `/api/leaderboard/players/total-runs/accounts/realm/${region}/${realm}/
${page}.json`;
  }
  if (scope === "regional" && region) {
    return `/api/leaderboard/players/total-runs/accounts/regional/${region}/${page
}.json`;
  }
  return `/api/leaderboard/players/total-runs/accounts/global/${page}.json`;
}

// account-grouped player leaderboard, per season
export function buildStaticAccountLeaderboardPath(
  scope: "global" | "regional" | "realm",
  region?: string,
  page: number = 1,
  opts?: { seasonId?: number; realmSlug?: string },
): string {
  const seasonId = opts?.seasonId ?? 1;
  if (scope === "realm" && region && opts?.realmSlug) {
    return `/api/leaderboard/season/${seasonId}/accounts/realm/${region}/${opts.re
almSlug}/${page}.json`;
  }
  if (scope === "regional" && region) {
    return `/api/leaderboard/season/${seasonId}/accounts/regional/${region}/${page
}.json`;
  }
  return `/api/leaderboard/season/${seasonId}/accounts/global/${page}.json`;
}

// cross-season tree; sits outside /season/N because the metric sums across season
s
export function buildStaticTotalRunsLeaderboardPath(
  scope: "global" | "regional" | "realm",
  region?: string,
  page: number = 1,
  opts?: { realmSlug?: string; classKey?: string },
): string {
  const cls = (opts?.classKey || "").toLowerCase();
  const realm = (opts?.realmSlug || "").toLowerCase();
  if (cls) {
    if (scope === "global") {
      return `/api/leaderboard/players/total-runs/class/${cls}/global/${page}.json
`;
    }
    if (scope === "regional" && region) {
      return `/api/leaderboard/players/total-runs/class/${cls}/regional/${region}/
${page}.json`;
    }
    if (scope === "realm" && region && realm) {
      return `/api/leaderboard/players/total-runs/class/${cls}/realm/${region}/${r
ealm}/${page}.json`;
    }
  }
  if (scope === "global") {
    return `/api/leaderboard/players/total-runs/global/${page}.json`;
  }
  if (scope === "regional" && region) {
    return `/api/leaderboard/players/total-runs/regional/${region}/${page}.json`;
  }
  if (scope === "realm" && region && realm) {
    return `/api/leaderboard/players/total-runs/realm/${region}/${realm}/${page}.j
son`;
  }
  return `/api/leaderboard/players/total-runs/global/${page}.json`;
}

export function buildStaticPlayerProfilePath(
  region: string,
  realmSlug: string,
  playerName: string,
): string {
  // Normalize to lowercase to match public/ directory structure on case-sensitive
 hosts
  const normRegion = (region || "").toLowerCase();
  const normRealm = (realmSlug || "").toLowerCase();
  const normPlayer = (playerName || "").toLowerCase();
  return `/api/player/${normRegion}/${normRealm}/${normPlayer}.json`;
}

export function buildStaticSearchIndexPath(shardNumber: number): string {
  const paddedNumber = shardNumber.toString().padStart(3, "0");
  return `/api/search/players-${paddedNumber}.json`;
}

// Status page utilities
export function filterByRegion<T extends { region?: string }>(
  items: T[],
  region: string,
): T[] {
  if (!region) return items;
  const normalized = region.toLowerCase();
  return items.filter(
    (item) => (item.region || "").toLowerCase() === normalized,
  );
}

export function safeArray<T>(value: any): T[] {
  return Array.isArray(value) ? value : [];
}

export const STATUS_CONSTANTS = {
  MODE: "status" as const,
  DELIMITER: "|" as const,
  REGION_REALM_SEPARATOR: "|" as const,
} as const;

// Common utility functions
import { DUNGEON_MAP, dungeonNameToSlug } fr
om "./wow-constants.js";

// Time formatting utilities
export function formatDuration(seconds: numb
er): string {
  const minutes = Math.floor(seconds / 60);
  const remainingSeconds = seconds % 60;
  return minutes > 0
    ? `${minutes}m ${remainingSeconds}s`
    : `${remainingSeconds}s`;
}

export function formatDurationMMSS(milliseco
nds: number): string {
  return `${Math.floor(milliseconds / 60000)
}:${String(Math.floor((milliseconds % 60000)
 / 1000)).padStart(2, "0")}`;
}

// Alias for clarity when working with milli
seconds
export const formatDurationFromMs = formatDu
rationMMSS;

export function formatTimestamp(timestamp: n
umber): string {
  return new Date(timestamp).toLocaleDateStr
ing("en-US", {
    month: "short",
    day: "numeric",
    hour: "2-digit",
    minute: "2-digit",
  });
}

// compact "Mar 27"; formatTimestamp's time-
of-day eats space in dense layouts
export function formatShortDate(timestamp: n
umber): string {
  return new Date(timestamp).toLocaleDateStr
ing("en-US", {
    month: "short",
    day: "numeric",
  });
}

export function formatSimulationDate(timesta
mp: string | number): string {
  return new Date(timestamp).toLocaleString(
"en-US", {
    year: "numeric",
    month: "long",
    day: "numeric",
    hour: "2-digit",
    minute: "2-digit",
    timeZoneName: "short",
  });
}

export function formatRaidBuffs(
  buffs: Record<string, boolean | number>,
): string {
  const MINOR_WORDS = new Set([
    "of",
    "the",
    "and",
    "to",
    "for",
    "a",
    "an",
    "in",
    "on",
    "with",
    "vs",
  ]);

  const normalize = (key: string): string =>
 {
    if (!key) return "";
    let s = String(key);
    // Drop trailing Count (e.g., skullBanne
rCount -> skullBanner)
    s = s.replace(/Count$/i, "");
    // Replace underscores with spaces and s
plit camelCase
    s = s.replace(/_/g, " ");
    s = s.replace(/([a-z])([A-Z])/g, "$1 $2"
);
    // Collapse whitespace
    s = s.replace(/\s+/g, " ").trim().toLowe
rCase();
    // Title-case while keeping minor words 
lowercase
    const words = s.split(" ");
    const out = words.map((w, i) => {
      if (i > 0 && MINOR_WORDS.has(w)) retur
n w;
      return w.charAt(0).toUpperCase() + w.s
lice(1);
    });
    return out.join(" ");
  };

  const activeBuffs = Object.entries(buffs)
    .filter(([_, value]) => value !== false 
&& value !== 0)
    .map(([key, value]) => {
      const name = normalize(key);
      return typeof value === "number" && va
lue > 1
        ? `${name} (${value})`
        : name;
    });
  return activeBuffs.join(", ");
}

export function formatRace(race: string): st
ring {
  if (!race) return "Unknown";
  let s = String(race).trim();
  // Strip common enum prefixes like "RaceOr
c" -> "Orc"
  if (/^Race[A-Z]/.test(s)) s = s.replace(/^
Race/, "");
  // Replace underscores with spaces and spl
it CamelCase into words
  s = s.replace(/_/g, " ");
  s = s.replace(/([a-z])([A-Z])/g, "$1 $2");
  // Normalize extra whitespace
  s = s.replace(/\s+/g, " ").trim();
  // Title case each word
  s = s.toLowerCase().replace(/\b\w/g, (l) =
> l.toUpperCase());
  return s;
}

// General string helpers
export function toTitleCase(input: string): 
string {
  if (!input) return "";
  const s = String(input).replace(/[_-]+/g, 
" ").trim().toLowerCase();
  return s.replace(/\b\w/g, (l) => l.toUpper
Case());
}

export function slugifyLabel(input: string):
 string {
  return String(input || "")
    .trim()
    .toLowerCase()
    .replace(/\s+/g, "_");
}

// DOM utilities
export function createElement(
  tag: string,
  className?: string,
  textContent?: string,
): HTMLElement {
  const element = document.createElement(tag
);
  if (className) element.className = classNa
me;
  if (textContent) element.textContent = tex
tContent;
  return element;
}

export function toggleElementVisibility(
  element: HTMLElement,
  show: boolean,
): void {
  element.style.display = show ? "block" : "
none";
}

// URL utilities
export function updateBrowserURL(url: string
): void {
  window.history.pushState({}, "", url);
}

export function getURLSearchParams(): URLSea
rchParams {
  return new URLSearchParams(window.location
.search);
}

export function buildLeaderboardURL(
  region: string,
  realm: string,
  dungeonSlug: string,
  page?: number,
  opts?: { season?: number; basePath?: strin
g },
): string {
  const season = Number(opts?.season ?? 1);
  const basePath = opts?.basePath ?? "challe
nge-mode";
  const seasonSegment = season > 0 ? `season
${season}` : "season1";

  const normalizedRegion = region || "global
";
  const normalizedRealm = realm && realm !==
 "" ? realm : "all";

  let baseURL: string;

  if (normalizedRegion === "global") {
    baseURL = `/${basePath}/${seasonSegment}
/global/${dungeonSlug}`;
  } else {
    const realmSlug = normalizedRealm || "al
l";
    baseURL = `/${basePath}/${seasonSegment}
/${normalizedRegion}/${realmSlug}/${dungeonS
lug}`;
  }

  if (page && page > 1) {
    baseURL += `?page=${page}`;
  }

  return baseURL;
}

export function buildPlayerProfileURL(
  region: string,
  realmSlug: string,
  playerName: string,
): string {
  return `/player/${region}/${realmSlug}/${p
layerName.toLowerCase()}`;
}

// Build Players tab URL path from filters
export function buildPlayersLeaderboardURL(
  scope: "global" | "regional" | "realm",
  opts?: {
    region?: string;
    realmSlug?: string;
    classKey?: string;
    page?: number;
  },
): string {
  const cls = (opts?.classKey || "").toLower
Case();
  const reg = (opts?.region || "").toLowerCa
se();
  const realm = (opts?.realmSlug || "").toLo
werCase();
  let path = "/challenge-mode/players";
  if (scope === "global") {
    if (cls) path += `/global/${cls}`;
    else path += `/global`;
  } else if (scope === "regional") {
    if (reg) path += `/${reg}`;
    else path += `/us`;
    if (cls) path += `/${cls}`;
  } else if (scope === "realm") {
    if (reg) path += `/${reg}`;
    if (realm) path += `/${realm}`;
    if (cls) path += `/${cls}`;
  }
  const page = opts?.page || 1;
  if (page > 1) path += `?page=${page}`;
  return path;
}

// Form utilities
export function getFormValue(elementId: stri
ng): string {
  const element = document.getElementById(el
ementId) as
    | HTMLInputElement
    | HTMLSelectElement;
  return element?.value || "";
}

export function setFormValue(elementId: stri
ng, value: string): void {
  const element = document.getElementById(el
ementId) as
    | HTMLInputElement
    | HTMLSelectElement;
  if (element) element.value = value;
}

// Validation utilities
export function validateRequiredFields(
  ...values: (string | undefined)[]
): boolean {
  return values.every((value) => value && va
lue.trim().length > 0);
}

// Error handling utilities
export function handleAPIError(error: Error,
 context: string): void {
  console.error(`${context} error:`, error);

  // Could extend this to show user-friendly
 error messages
  const errorMessage = error.message.include
s("404")
    ? "Data not found"
    : error.message.includes("500")
      ? "Server error - please try again lat
er"
      : "An error occurred loading data";

  console.warn(`User-friendly error: ${error
Message}`);
}

// (Removed legacy loading state helpers in 
favor of <LoadingState /> component)

// Debouncing utility for search inputs
export function debounce<T extends (...args:
 any[]) => void>(
  func: T,
  wait: number,
): (...args: Parameters<T>) => void {
  let timeout: NodeJS.Timeout;

  return (...args: Parameters<T>) => {
    clearTimeout(timeout);
    timeout = setTimeout(() => func(...args)
, wait);
  };
}

// Local storage utilities
export function saveToLocalStorage(key: stri
ng, data: any): void {
  try {
    localStorage.setItem(key, JSON.stringify
(data));
  } catch (error) {
    console.warn("Failed to save to localSto
rage:", error);
  }
}

export function loadFromLocalStorage<T>(key:
 string): T | null {
  try {
    const item = localStorage.getItem(key);
    return item ? JSON.parse(item) : null;
  } catch (error) {
    console.warn("Failed to load from localS
torage:", error);
    return null;
  }
}

// Array utilities
export function chunk<T>(array: T[], size: n
umber): T[][] {
  const chunks: T[][] = [];
  for (let i = 0; i < array.length; i += siz
e) {
    chunks.push(array.slice(i, i + size));
  }
  return chunks;
}

export function unique<T>(array: T[]): T[] {
  return Array.from(new Set(array));
}

// Object utilities
export function isEmpty(obj: any): boolean {
  if (!obj) return true;
  if (Array.isArray(obj)) return obj.length 
=== 0;
  if (typeof obj === "object") return Object
.keys(obj).length === 0;
  return false;
}

// Static API utilities for file-based endpo
ints
export function dungeonIdToSlug(dungeonId: n
umber): string {
  const dungeonName = DUNGEON_MAP[dungeonId 
as keyof typeof DUNGEON_MAP];
  if (!dungeonName) {
    throw new Error(`Unknown dungeon ID: ${d
ungeonId}`);
  }
  return dungeonNameToSlug(dungeonName);
}

// Build static API file paths
export function buildStaticLeaderboardPath(
  region: string,
  realm: string,
  dungeonId: number,
  page: number = 1,
  seasonId: number = 1,
): string {
  const dungeonSlug = dungeonIdToSlug(dungeo
nId);

  if (region === "global") {
    return `/api/leaderboard/season/${season
Id}/global/${dungeonSlug}/${page}.json`;
  } else if (realm === "all") {
    return `/api/leaderboard/season/${season
Id}/${region}/all/${dungeonSlug}/${page}.jso
n`;
  } else {
    return `/api/leaderboard/season/${season
Id}/${region}/${realm}/${dungeonSlug}/${page
}.json`;
  }
}

export function buildStaticPlayerLeaderboard
Path(
  scope: "global" | "regional" | "realm",
  region?: string,
  page: number = 1,
  opts?: { realmSlug?: string; classKey?: st
ring; seasonId?: number },
): string {
  const cls = (opts?.classKey || "").toLower
Case();
  const realm = (opts?.realmSlug || "").toLo
werCase();
  const seasonId = opts?.seasonId ?? 1;

  // Class-filtered variants
  if (cls) {
    if (scope === "global") {
      return `/api/leaderboard/season/${seas
onId}/players/class/${cls}/global/${page}.js
on`;
    }
    if (scope === "regional" && region) {
      return `/api/leaderboard/season/${seas
onId}/players/class/${cls}/regional/${region
}/${page}.json`;
    }
    if (scope === "realm" && region && realm
) {
      return `/api/leaderboard/season/${seas
onId}/players/class/${cls}/realm/${region}/$
{realm}/${page}.json`;
    }
  }

  // Unfiltered
  if (scope === "global") {
    return `/api/leaderboard/season/${season
Id}/players/global/${page}.json`;
  }
  if (scope === "regional" && region) {
    return `/api/leaderboard/season/${season
Id}/players/regional/${region}/${page}.json`
;
  }
  if (scope === "realm" && region && realm) 
{
    return `/api/leaderboard/season/${season
Id}/players/realm/${region}/${realm}/${page}
.json`;
  }
  // Fallback to global
  return `/api/leaderboard/season/${seasonId
}/players/global/${page}.json`;
}

// Unified leaderboard URL builder for the m
erged Players/Characters page.
// Picks the correct static JSON path for an
y (view, sort, season, scope)
// combination. Existing trees are reused wh
en possible; new combinations
// (all-time time, per-season runs) hit the 
Phase 1 generators.
//
// Notes on URL drift between trees:
//   * Per-char per-season time and per-acco
unt per-season time still live at
//     /season/{N}/players/ and /season/{N}/
accounts/ - the migration to
//     /season{N}/{view}/by-{sort}/ happens 
in Phase 3.
//   * Per-char and per-account all-time run
s still live at
//     /players/total-runs/ - same reason.
type LeaderboardView = "player" | "character
";
type LeaderboardSort = "time" | "runs";

export function buildUnifiedLeaderboardPath(
  view: LeaderboardView,
  sort: LeaderboardSort,
  season: number | "all-time",
  scope: "global" | "regional" | "realm" | "
class",
  opts?: {
    region?: string;
    realmSlug?: string;
    classKey?: string;
    page?: number;
  },
): string {
  const page = opts?.page ?? 1;
  const region = (opts?.region || "").toLowe
rCase();
  const realm = (opts?.realmSlug || "").toLo
werCase();
  const cls = (opts?.classKey || "").toLower
Case();

  // Build the scope segments common to most
 paths.
  const scopeSegments = (() => {
    if (scope === "global") return "global";
    if (scope === "regional") return `region
al/${region}`;
    if (scope === "realm") return `realm/${r
egion}/${realm}`;
    if (scope === "class") {
      // Class scope is followed by an inner
 geo (only valid for character view).
      // Caller signals which geo via region
/realm being set: realm > region > global.
      const inner = realm
        ? `realm/${region}/${realm}`
        : region
          ? `regional/${region}`
          : "global";
      return `class/${cls}/${inner}`;
    }
    return "global";
  })();

  // All-Time time ? Phase 1 unified tree.
  if (season === "all-time" && sort === "tim
e") {
    const v = view === "player" ? "players" 
: "characters";
    return `/api/leaderboard/all-time/${v}/b
y-time/${scopeSegments}/${page}.json`;
  }
  // All-Time runs ? existing cross-season T
otal Runs trees.
  if (season === "all-time" && sort === "run
s") {
    if (view === "player") {
      // Account total runs (no class scope 
- accounts span classes)
      if (scope === "realm")
        return `/api/leaderboard/players/tot
al-runs/accounts/realm/${region}/${realm}/${
page}.json`;
      if (scope === "regional")
        return `/api/leaderboard/players/tot
al-runs/accounts/regional/${region}/${page}.
json`;
      return `/api/leaderboard/players/total
-runs/accounts/global/${page}.json`;
    }
    // Per-char total runs
    if (scope === "class")
      return buildStaticTotalRunsLeaderboard
Path(
        realm ? "realm" : region ? "regional
" : "global",
        region,
        page,
        { realmSlug: realm || undefined, cla
ssKey: cls },
      );
    if (scope === "realm")
      return `/api/leaderboard/players/total
-runs/realm/${region}/${realm}/${page}.json`
;
    if (scope === "regional")
      return `/api/leaderboard/players/total
-runs/regional/${region}/${page}.json`;
    return `/api/leaderboard/players/total-r
uns/global/${page}.json`;
  }
  // Per-season time + per-season runs.
  // Note season=all-time doesn't reach here
.
  const seasonNum = season as number;
  if (sort === "time") {
    // Existing trees (Phase 3 will migrate 
them under /season{N}/.../by-time/).
    if (view === "player") {
      if (scope === "realm")
        return `/api/leaderboard/season/${se
asonNum}/accounts/realm/${region}/${realm}/$
{page}.json`;
      if (scope === "regional")
        return `/api/leaderboard/season/${se
asonNum}/accounts/regional/${region}/${page}
.json`;
      return `/api/leaderboard/season/${seas
onNum}/accounts/global/${page}.json`;
    }
    // character view, time, per-season ? ex
isting per-char per-season tree
    return buildStaticPlayerLeaderboardPath(
      scope === "global"
        ? "global"
        : scope === "regional"
          ? "regional"
          : scope === "realm"
            ? "realm"
            : "global",
      region,
      page,
      {
        realmSlug: realm || undefined,
        classKey: cls || undefined,
        seasonId: seasonNum,
      },
    );
  }
  // sort === "runs" + per-season ? Phase 1 
new tree.
  const v = view === "player" ? "players" : 
"characters";
  return `/api/leaderboard/season${seasonNum
}/${v}/by-runs/${scopeSegments}/${page}.json
`;
}

// Account-grouped cross-season Total Runs (
counterpart to the per-character
// total runs leaderboard, used by the Playe
r toggle on /players/total-runs/).
export function buildStaticAccountTotalRunsP
ath(
  scope: "global" | "regional" | "realm",
  region?: string,
  page: number = 1,
  opts?: { realmSlug?: string },
): string {
  const realm = (opts?.realmSlug || "").toLo
werCase();
  if (scope === "realm" && region && realm) 
{
    return `/api/leaderboard/players/total-r
uns/accounts/realm/${region}/${realm}/${page
}.json`;
  }
  if (scope === "regional" && region) {
    return `/api/leaderboard/players/total-r
uns/accounts/regional/${region}/${page}.json
`;
  }
  return `/api/leaderboard/players/total-run
s/accounts/global/${page}.json`;
}

// account-grouped player leaderboard, per s
eason
export function buildStaticAccountLeaderboar
dPath(
  scope: "global" | "regional" | "realm",
  region?: string,
  page: number = 1,
  opts?: { seasonId?: number; realmSlug?: st
ring },
): string {
  const seasonId = opts?.seasonId ?? 1;
  if (scope === "realm" && region && opts?.r
ealmSlug) {
    return `/api/leaderboard/season/${season
Id}/accounts/realm/${region}/${opts.realmSlu
g}/${page}.json`;
  }
  if (scope === "regional" && region) {
    return `/api/leaderboard/season/${season
Id}/accounts/regional/${region}/${page}.json
`;
  }
  return `/api/leaderboard/season/${seasonId
}/accounts/global/${page}.json`;
}

// cross-season tree; sits outside /season/N
 because the metric sums across seasons
export function buildStaticTotalRunsLeaderbo
ardPath(
  scope: "global" | "regional" | "realm",
  region?: string,
  page: number = 1,
  opts?: { realmSlug?: string; classKey?: st
ring },
): string {
  const cls = (opts?.classKey || "").toLower
Case();
  const realm = (opts?.realmSlug || "").toLo
werCase();
  if (cls) {
    if (scope === "global") {
      return `/api/leaderboard/players/total
-runs/class/${cls}/global/${page}.json`;
    }
    if (scope === "regional" && region) {
      return `/api/leaderboard/players/total
-runs/class/${cls}/regional/${region}/${page
}.json`;
    }
    if (scope === "realm" && region && realm
) {
      return `/api/leaderboard/players/total
-runs/class/${cls}/realm/${region}/${realm}/
${page}.json`;
    }
  }
  if (scope === "global") {
    return `/api/leaderboard/players/total-r
uns/global/${page}.json`;
  }
  if (scope === "regional" && region) {
    return `/api/leaderboard/players/total-r
uns/regional/${region}/${page}.json`;
  }
  if (scope === "realm" && region && realm) 
{
    return `/api/leaderboard/players/total-r
uns/realm/${region}/${realm}/${page}.json`;
  }
  return `/api/leaderboard/players/total-run
s/global/${page}.json`;
}

export function buildStaticPlayerProfilePath
(
  region: string,
  realmSlug: string,
  playerName: string,
): string {
  // Normalize to lowercase to match public/
 directory structure on case-sensitive hosts
  const normRegion = (region || "").toLowerC
ase();
  const normRealm = (realmSlug || "").toLowe
rCase();
  const normPlayer = (playerName || "").toLo
werCase();
  return `/api/player/${normRegion}/${normRe
alm}/${normPlayer}.json`;
}

export function buildStaticSearchIndexPath(s
hardNumber: number): string {
  const paddedNumber = shardNumber.toString(
).padStart(3, "0");
  return `/api/search/players-${paddedNumber
}.json`;
}

// Status page utilities
export function filterByRegion<T extends { r
egion?: string }>(
  items: T[],
  region: string,
): T[] {
  if (!region) return items;
  const normalized = region.toLowerCase();
  return items.filter(
    (item) => (item.region || "").toLowerCas
e() === normalized,
  );
}

export function safeArray<T>(value: any): T[
] {
  return Array.isArray(value) ? value : [];
}

export const STATUS_CONSTANTS = {
  MODE: "status" as const,
  DELIMITER: "|" as const,
  REGION_REALM_SEPARATOR: "|" as const,
} as const;
 
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET