import type { FetchedStandingsRecord, StandingsSyncAdapter } from "./types";

const WNBA_STANDINGS_URL =
  "https://site.api.espn.com/apis/v2/sports/basketball/wnba/standings";

/**
 * Returns all registered WNBA teams (including pre-season expansion teams not yet
 * in standings). Used to supplement the standings response so every team gets a record.
 */
const WNBA_TEAMS_URL =
  "https://site.api.espn.com/apis/site/v2/sports/basketball/wnba/teams";

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[];
}

interface EspnTeamItem {
  team: {
    id: string;
    displayName: string;
    isActive?: boolean;
  };
}

interface EspnTeamsResponse {
  sports?: Array<{
    leagues?: Array<{
      teams?: EspnTeamItem[];
    }>;
  }>;
}

function statsMap(stats: EspnStat[]): Map<string, EspnStat> {
  const map = new Map<string, EspnStat>();
  for (const stat of stats) {
    map.set(stat.name, stat);
  }
  return map;
}

/**
 * Flatten the ESPN standings response.
 * WNBA has conferences (East/West) but no divisions — entries are either directly
 * under the conference group or nested one level deeper (ESPN sometimes wraps
 * conferences in a single top-level group).  Handles both layouts.
 */
function flattenEspnStandings(
  response: EspnStandingsResponse
): Array<{ entry: EspnStandingsEntry; conference: string }> {
  const results: Array<{ entry: EspnStandingsEntry; conference: string }> = [];

  for (const topGroup of response.children ?? []) {
    if (topGroup.children && topGroup.children.length > 0) {
      // Top-level group has sub-groups — each sub-group is a conference (no divisions).
      for (const confGroup of topGroup.children) {
        const conferenceName = confGroup.name ?? topGroup.name ?? "";
        for (const entry of confGroup.standings?.entries ?? []) {
          results.push({ entry, conference: conferenceName });
        }
      }
    } else {
      // Top-level group is the conference itself.
      const conferenceName = topGroup.name ?? "";
      for (const entry of topGroup.standings?.entries ?? []) {
        results.push({ entry, conference: conferenceName });
      }
    }
  }

  return results;
}

/** Parse a standings entry into a FetchedStandingsRecord (rank assigned by caller). */
function parseEntry(
  entry: EspnStandingsEntry,
  conference: string,
  leagueRank: number
): 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;
  const lastTen =
    sm.get("Last Ten Games")?.displayValue ??
    sm.get("L10")?.displayValue ??
    undefined;
  const homeRecord = sm.get("Home")?.displayValue ?? undefined;
  const awayRecord = sm.get("Road")?.displayValue ?? undefined;

  const playoffSeedStat = sm.get("playoffSeed");
  const conferenceRank =
    playoffSeedStat?.value !== undefined && playoffSeedStat.value !== null
      ? Math.round(playoffSeedStat.value)
      : playoffSeedStat?.displayValue
      ? parseInt(playoffSeedStat.displayValue, 10) || undefined
      : undefined;

  // SRS proxy: average point differential per game.
  // ESPN exposes "avgPointsFor" and "avgPointsAgainst" (or "pointsFor"/"pointsAgainst").
  // If unavailable, fall back to null so the simulator uses the league-average Elo (1500).
  const ptFor =
    sm.get("avgPointsFor")?.value ??
    sm.get("pointsFor")?.value ??
    null;
  const ptAgainst =
    sm.get("avgPointsAgainst")?.value ??
    sm.get("pointsAgainst")?.value ??
    null;
  const srs =
    ptFor !== null && ptFor !== undefined &&
    ptAgainst !== null && ptAgainst !== undefined
      ? Math.round((ptFor - ptAgainst) * 100) / 100
      : null;

  return {
    teamName: entry.team.displayName,
    externalTeamId: entry.team.id,
    wins: Math.round(wins),
    losses: Math.round(losses),
    winPct: winPercent,
    gamesPlayed: Math.round(wins) + Math.round(losses),
    gamesBack: gamesBehind,
    conference: conference || undefined,
    conferenceRank,
    leagueRank,
    streak,
    lastTen,
    homeRecord,
    awayRecord,
    srs,
  };
}

export class WnbaStandingsAdapter implements StandingsSyncAdapter {
  async fetchStandings(): Promise<FetchedStandingsRecord[]> {
    // Fetch both endpoints in parallel.
    const [standingsResponse, teamsResponse] = await Promise.all([
      fetch(WNBA_STANDINGS_URL),
      fetch(WNBA_TEAMS_URL),
    ]);

    if (!standingsResponse.ok) {
      throw new Error(
        `WNBA standings API returned ${standingsResponse.status}: ${standingsResponse.statusText}`
      );
    }
    if (!teamsResponse.ok) {
      throw new Error(
        `WNBA teams API returned ${teamsResponse.status}: ${teamsResponse.statusText}`
      );
    }

    const [standingsJson, teamsJson] = await Promise.all([
      standingsResponse.json() as Promise<EspnStandingsResponse>,
      teamsResponse.json() as Promise<EspnTeamsResponse>,
    ]);

    // Build standings records from the standings endpoint, sorted by wins desc.
    // Pre-parse each entry once so statsMap isn't rebuilt multiple times per entry.
    const flattened = flattenEspnStandings(standingsJson).map(({ entry, conference }) => ({
      entry,
      conference,
      sm: statsMap(entry.stats),
    }));
    const sortedEntries = [...flattened].toSorted((a, b) => {
      const winsA = a.sm.get("wins")?.value ?? 0;
      const winsB = b.sm.get("wins")?.value ?? 0;
      if (winsB !== winsA) return winsB - winsA;
      const lossA = a.sm.get("losses")?.value ?? 0;
      const lossB = b.sm.get("losses")?.value ?? 0;
      return lossA - lossB;
    });

    // Map externalTeamId → parsed record (so we can detect which teams are missing).
    const recordsByTeamId = new Map<string, FetchedStandingsRecord>();
    sortedEntries.forEach(({ entry, conference }, idx) => {
      recordsByTeamId.set(
        entry.team.id,
        parseEntry(entry, conference, idx + 1)
      );
    });

    // Extract the full team list from the teams endpoint.
    const allTeams: Array<{ id: string; displayName: string }> = (
      teamsJson.sports?.[0]?.leagues?.[0]?.teams ?? []
    )
      .filter((t) => t.team.isActive !== false)
      .map((t) => ({ id: t.team.id, displayName: t.team.displayName }));

    if (allTeams.length === 0 && recordsByTeamId.size === 0) {
      throw new Error(
        "WNBA standings API returned no entries — response shape may have changed"
      );
    }

    // Merge: use standing records where available; fill in zero-records for
    // expansion teams not yet in the standings (pre-season or mid-season addition).
    const results: FetchedStandingsRecord[] = [];
    const seenIds = new Set<string>();

    // First, add all teams that have standings data (preserving their rank).
    for (const record of recordsByTeamId.values()) {
      results.push(record);
      seenIds.add(record.externalTeamId);
    }

    // Then, append zero-records for any team in the teams list but not in standings.
    let expansionRank = results.length + 1;
    for (const team of allTeams) {
      if (!seenIds.has(team.id)) {
        results.push({
          teamName: team.displayName,
          externalTeamId: team.id,
          wins: 0,
          losses: 0,
          winPct: 0,
          gamesPlayed: 0,
          leagueRank: expansionRank++,
          srs: null,
        });
      }
    }

    // If the teams endpoint returned nothing (API change), fall back to standings only.
    if (results.length === 0) {
      throw new Error(
        "WNBA standings API returned no entries — response shape may have changed"
      );
    }

    return results;
  }
}
