import type { FetchedStandingsRecord, StandingsSyncAdapter } from "./types"; const NBA_STANDINGS_URL = "https://site.api.espn.com/apis/v2/sports/basketball/nba/standings"; interface EspnStat { name: string; displayName?: string; shortDisplayName?: string; description?: string; abbreviation?: string; type?: string; value?: number; displayValue?: string; } interface EspnTeam { id: string; displayName: string; shortDisplayName?: string; abbreviation?: string; location?: string; name?: string; } interface EspnStandingsEntry { team: EspnTeam; stats: EspnStat[]; } interface EspnStandingsGroup { name?: string; standings?: { entries?: EspnStandingsEntry[] }; children?: EspnStandingsGroup[]; } interface EspnStandingsResponse { children?: EspnStandingsGroup[]; } function statsMap(stats: EspnStat[]): Map { const map = new Map(); for (const stat of stats) { map.set(stat.name, stat); } return map; } /** * Flatten the ESPN standings response (conference → division → entries). * Returns enriched entries with conference and division names attached. */ function flattenEspnStandings( response: EspnStandingsResponse ): Array<{ entry: EspnStandingsEntry; conference: string; division: string }> { const results: Array<{ entry: EspnStandingsEntry; conference: string; division: string; }> = []; for (const confGroup of response.children ?? []) { const conferenceName = confGroup.name ?? ""; // Some ESPN responses have nested division children if (confGroup.children && confGroup.children.length > 0) { for (const divGroup of confGroup.children) { const divisionName = divGroup.name ?? ""; for (const entry of divGroup.standings?.entries ?? []) { results.push({ entry, conference: conferenceName, division: divisionName }); } } } else { // Flat conference without division sub-groups for (const entry of confGroup.standings?.entries ?? []) { results.push({ entry, conference: conferenceName, division: "" }); } } } return results; } export class NbaStandingsAdapter implements StandingsSyncAdapter { async fetchStandings(): Promise { const response = await fetch(NBA_STANDINGS_URL); if (!response.ok) { throw new Error(`NBA standings API returned ${response.status}: ${response.statusText}`); } const json = (await response.json()) as EspnStandingsResponse; const flattened = flattenEspnStandings(json); if (flattened.length === 0) { throw new Error("NBA standings API returned no entries — response shape may have changed"); } // Assign league rank by sorting on wins desc, then losses asc const sorted = [...flattened].sort((a, b) => { const winsA = statsMap(a.entry.stats).get("wins")?.value ?? 0; const winsB = statsMap(b.entry.stats).get("wins")?.value ?? 0; if (winsB !== winsA) return winsB - winsA; const lossA = statsMap(a.entry.stats).get("losses")?.value ?? 0; const lossB = statsMap(b.entry.stats).get("losses")?.value ?? 0; return lossA - lossB; }); return sorted.map(({ entry, conference, division }, leagueIdx): FetchedStandingsRecord => { const sm = statsMap(entry.stats); const wins = sm.get("wins")?.value ?? 0; const losses = sm.get("losses")?.value ?? 0; const winPercent = sm.get("winPercent")?.value ?? sm.get("winPct")?.value ?? 0; const gamesBehind = sm.get("gamesBehind")?.value; const streak = sm.get("streak")?.displayValue ?? sm.get("streakSummary")?.displayValue; // ESPN returns last-10 as "Last Ten Games" with a displayValue like "7-3" const lastTen = sm.get("Last Ten Games")?.displayValue ?? sm.get("L10")?.displayValue ?? undefined; // Seed within conference — ESPN key is "playoffSeed", value is a float (1.0, 2.0 ...) const playoffSeedStat = sm.get("playoffSeed"); const conferenceRank = playoffSeedStat?.value != null ? Math.round(playoffSeedStat.value) : playoffSeedStat?.displayValue ? parseInt(playoffSeedStat.displayValue, 10) || undefined : undefined; const gamesPlayed = wins + losses; // Home/road records — ESPN returns these as displayValue strings (e.g. "24-17") const homeRecord = sm.get("Home")?.displayValue ?? undefined; const awayRecord = sm.get("Road")?.displayValue ?? undefined; return { teamName: entry.team.displayName, externalTeamId: entry.team.id, wins: Math.round(wins), losses: Math.round(losses), winPct: winPercent, gamesPlayed, gamesBack: gamesBehind, conference, division: division || undefined, conferenceRank, leagueRank: leagueIdx + 1, streak, lastTen, homeRecord, awayRecord, }; }); } }