"""Normalize NHL /v1/playoff-bracket payloads for the bracket template. The NHL bracket uses stable series letters: A,B,C,D = Round 1 East E,F,G,H = Round 1 West I,J = Round 2 East K,L = Round 2 West M = Conf Final East N = Conf Final West O = Stanley Cup Final """ from app.playoff import ROUND_LABELS EAST_R1 = ["A", "B", "C", "D"] WEST_R1 = ["E", "F", "G", "H"] EAST_R2 = ["I", "J"] WEST_R2 = ["K", "L"] EAST_CF = ["M"] WEST_CF = ["N"] CUP_FINAL = ["O"] def build_bracket_view(year, bracket_payload, fetched_at=None): """Shape the raw bracket API payload for bracket.html. Returns a dict of rounds grouped by conference, plus a flat `matchups` list keyed by letter for the mobile accordion. Missing letters render as empty placeholder slots so the grid stays visually complete before upsets decide. """ series_by_letter = {} for s in (bracket_payload or {}).get("series", []): letter = s.get("seriesLetter") if letter: series_by_letter[letter] = s def slot(letter): return _matchup(year, letter, series_by_letter.get(letter)) east_r1 = [slot(ltr) for ltr in EAST_R1] west_r1 = [slot(ltr) for ltr in WEST_R1] east_r2 = [slot(ltr) for ltr in EAST_R2] west_r2 = [slot(ltr) for ltr in WEST_R2] east_cf = [slot(ltr) for ltr in EAST_CF] west_cf = [slot(ltr) for ltr in WEST_CF] cup = [slot(ltr) for ltr in CUP_FINAL] return { "year": year, "fetched_at": fetched_at, "bracket_logo": (bracket_payload or {}).get("bracketLogo"), "east_r1": east_r1, "west_r1": west_r1, "east_r2": east_r2, "west_r2": west_r2, "east_cf": east_cf, "west_cf": west_cf, "cup": cup, "rounds": [ {"label": ROUND_LABELS[1], "east": east_r1, "west": west_r1}, {"label": ROUND_LABELS[2], "east": east_r2, "west": west_r2}, {"label": ROUND_LABELS[3], "east": east_cf, "west": west_cf}, {"label": ROUND_LABELS[4], "cup": cup}, ], } def _matchup(year, letter, series): """Render-ready dict for one bracket slot. Empty when the series is unknown.""" if not series: return { "letter": letter, "series_id": f"{year}-{letter}", "empty": True, "top": None, "bottom": None, "top_wins": 0, "bottom_wins": 0, "round": None, "winner_abbrev": None, "state": "pending", } top = series.get("topSeedTeam") or {} bot = series.get("bottomSeedTeam") or {} top_wins = _to_int(series.get("topSeedWins")) bot_wins = _to_int(series.get("bottomSeedWins")) winning_id = series.get("winningTeamId") winner_abbrev = None if winning_id is not None: if top.get("id") == winning_id: winner_abbrev = top.get("abbrev") elif bot.get("id") == winning_id: winner_abbrev = bot.get("abbrev") if winner_abbrev: state = "complete" elif top_wins > 0 or bot_wins > 0: state = "active" else: state = "upcoming" return { "letter": letter, "series_id": f"{year}-{letter}", "empty": False, "top": _team(top, series.get("topSeedRankAbbrev")), "bottom": _team(bot, series.get("bottomSeedRankAbbrev")), "top_wins": top_wins, "bottom_wins": bot_wins, "round": series.get("playoffRound"), "winner_abbrev": winner_abbrev, "state": state, } def _team(team, seed_abbrev=None): if not team: return None return { "id": team.get("id"), "abbrev": team.get("abbrev"), "name": (team.get("name") or {}).get("default"), "common_name": (team.get("commonName") or {}).get("default"), "logo": team.get("darkLogo") or team.get("logo"), "seed": seed_abbrev, } def _to_int(v, default=0): try: return int(v) except (TypeError, ValueError): return default