from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session, joinedload from typing import List from datetime import datetime from ..database import get_db from ..models import Event, EventPlayer, Stage, Match, Player, Court from ..schemas import ( EventCreate, EventUpdate, EventPlayerAdd, EventPlayerOut, StageCreate, StageUpdate, StageOut, MatchOut ) from ..bracket import calculate_stages, generate_single_elim_bracket, generate_open_schedule, generate_round_robin router = APIRouter(prefix="/api/events", tags=["events"]) def _event_with_details(event_id: int, db: Session): return db.query(Event).options( joinedload(Event.event_players).joinedload(EventPlayer.player), joinedload(Event.stages).joinedload(Stage.matches) ).filter(Event.id == event_id).first() @router.get("/") def list_events(db: Session = Depends(get_db)): events = db.query(Event).order_by(Event.created_at.desc()).all() return [{"id": e.id, "name": e.name, "format": e.format, "status": e.status, "courts_count": e.courts_count, "created_at": e.created_at, "player_count": len(e.event_players), "stage_count": len(e.stages)} for e in events] @router.post("/") def create_event(data: EventCreate, db: Session = Depends(get_db)): event = Event(**data.model_dump()) db.add(event) db.commit() db.refresh(event) return {"id": event.id, "name": event.name, "format": event.format, "status": event.status, "courts_count": event.courts_count} @router.get("/calculate") def calculate_event(players: int, format: str = "Singles", courts: int = 1): return calculate_stages(players, format, courts) @router.get("/{event_id}") def get_event(event_id: int, db: Session = Depends(get_db)): event = _event_with_details(event_id, db) if not event: raise HTTPException(status_code=404, detail="Event not found") stages_data = [] for stage in event.stages: matches_data = [] for m in stage.matches: p1 = db.query(Player).filter(Player.id == m.player1_id).first() if m.player1_id else None p2 = db.query(Player).filter(Player.id == m.player2_id).first() if m.player2_id else None court = db.query(Court).filter(Court.id == m.court_id).first() if m.court_id else None matches_data.append({ "id": m.id, "round_number": m.round_number, "match_number": m.match_number, "player1_id": m.player1_id, "player2_id": m.player2_id, "team1_players": m.team1_players, "team2_players": m.team2_players, "player1_name": p1.name if p1 else None, "player2_name": p2.name if p2 else None, "score1": m.score1, "score2": m.score2, "winner_id": m.winner_id, "winner_team": m.winner_team, "status": m.status, "bracket_position": m.bracket_position, "next_winner_match_id": m.next_winner_match_id, "next_loser_match_id": m.next_loser_match_id, "court_id": m.court_id, "court_name": court.name if court else None, "started_at": m.started_at, "completed_at": m.completed_at, }) stages_data.append({ "id": stage.id, "name": stage.name, "match_type": stage.match_type, "rounds": stage.rounds, "advance_count": stage.advance_count, "status": stage.status, "order": stage.order, "matches": matches_data, }) players_data = [{ "id": ep.id, "player_id": ep.player_id, "skill_level_override": ep.skill_level_override, "player": {"id": ep.player.id, "name": ep.player.name, "skill_level": ep.player.skill_level, "elo": ep.player.elo, "matches_played": ep.player.matches_played, "wins": ep.player.wins, "losses": ep.player.losses} } for ep in event.event_players] return { "id": event.id, "name": event.name, "format": event.format, "courts_count": event.courts_count, "status": event.status, "current_stage_id": event.current_stage_id, "created_at": event.created_at, "started_at": event.started_at, "players": players_data, "stages": stages_data, } @router.put("/{event_id}") def update_event(event_id: int, data: EventUpdate, db: Session = Depends(get_db)): event = db.query(Event).filter(Event.id == event_id).first() if not event: raise HTTPException(status_code=404, detail="Event not found") for k, v in data.model_dump(exclude_none=True).items(): setattr(event, k, v) db.commit() return {"ok": True} @router.post("/{event_id}/players") def add_player_to_event(event_id: int, data: EventPlayerAdd, db: Session = Depends(get_db)): event = db.query(Event).filter(Event.id == event_id).first() if not event: raise HTTPException(status_code=404, detail="Event not found") player = db.query(Player).filter(Player.id == data.player_id).first() if not player: raise HTTPException(status_code=404, detail="Player not found") existing = db.query(EventPlayer).filter( EventPlayer.event_id == event_id, EventPlayer.player_id == data.player_id ).first() if existing: raise HTTPException(status_code=400, detail="Player already in event") ep = EventPlayer(event_id=event_id, player_id=data.player_id, skill_level_override=data.skill_level_override) db.add(ep) db.commit() db.refresh(ep) return {"id": ep.id, "player_id": ep.player_id} @router.delete("/{event_id}/players/{player_id}") def remove_player_from_event(event_id: int, player_id: int, db: Session = Depends(get_db)): ep = db.query(EventPlayer).filter( EventPlayer.event_id == event_id, EventPlayer.player_id == player_id ).first() if not ep: raise HTTPException(status_code=404, detail="Player not in event") db.delete(ep) db.commit() return {"ok": True} @router.post("/{event_id}/stages") def add_stage(event_id: int, data: StageCreate, db: Session = Depends(get_db)): event = db.query(Event).filter(Event.id == event_id).first() if not event: raise HTTPException(status_code=404, detail="Event not found") stage = Stage(event_id=event_id, **data.model_dump()) db.add(stage) db.commit() db.refresh(stage) return {"id": stage.id, "name": stage.name, "match_type": stage.match_type, "rounds": stage.rounds, "advance_count": stage.advance_count, "status": stage.status} @router.put("/{event_id}/stages/{stage_id}") def update_stage(event_id: int, stage_id: int, data: StageUpdate, db: Session = Depends(get_db)): stage = db.query(Stage).filter(Stage.id == stage_id, Stage.event_id == event_id).first() if not stage: raise HTTPException(status_code=404, detail="Stage not found") for k, v in data.model_dump(exclude_none=True).items(): setattr(stage, k, v) db.commit() return {"ok": True} @router.post("/{event_id}/stages/{stage_id}/generate") def generate_stage_matches(event_id: int, stage_id: int, db: Session = Depends(get_db)): """Generate matches for a stage""" stage = db.query(Stage).filter(Stage.id == stage_id, Stage.event_id == event_id).first() if not stage: raise HTTPException(status_code=404, detail="Stage not found") event = db.query(Event).filter(Event.id == event_id).first() # Delete existing matches for this stage db.query(Match).filter(Match.stage_id == stage_id).delete() db.commit() # Get players for this event event_players = db.query(EventPlayer).filter(EventPlayer.event_id == event_id).all() player_ids = [ep.player_id for ep in event_players] if len(player_ids) < 2: raise HTTPException(status_code=400, detail="Need at least 2 players") match_type = stage.match_type matches_to_create = [] if match_type in ["Open", "Open Match"]: matches_raw = generate_open_schedule(player_ids, stage.rounds, event.courts_count) for m in matches_raw: matches_to_create.append(Match( stage_id=stage_id, round_number=m["round_number"], match_number=m["match_number"], player1_id=m["player1_id"], player2_id=m["player2_id"], status=m["status"], bracket_position=m.get("bracket_position", "main"), )) elif match_type == "Round Robin": pairings = generate_round_robin(player_ids) for i, (p1, p2) in enumerate(pairings): matches_to_create.append(Match( stage_id=stage_id, round_number=1, match_number=i + 1, player1_id=p1, player2_id=p2, status="scheduled", bracket_position="main", )) elif match_type in ["Tournament", "Double Elimination"]: bracket_matches = generate_single_elim_bracket(player_ids) # Create matches and track temp IDs created = {} # First pass - create all match objects db_matches = [] for bm in bracket_matches: m = Match( stage_id=stage_id, round_number=bm["round_number"], match_number=bm["match_number"], player1_id=bm.get("player1_id"), player2_id=bm.get("player2_id"), status=bm["status"], bracket_position=bm.get("bracket_position", "winners"), ) db.add(m) db.flush() created[bm["temp_id"]] = m db_matches.append((bm, m)) # Second pass - link next_winner_match_id for bm, m in db_matches: if bm.get("next_winner_temp_id") and bm["next_winner_temp_id"] in created: m.next_winner_match_id = created[bm["next_winner_temp_id"]].id db.commit() return {"ok": True, "match_count": len(bracket_matches)} elif match_type == "Skill-Based": # Group players by skill level and create matches within same skill groups players = db.query(Player).filter(Player.id.in_(player_ids)).all() skill_order = {"Beginner": 0, "Intermediate": 1, "Advanced": 2, "Elite": 3} players_sorted = sorted(players, key=lambda p: skill_order.get(p.skill_level, 0)) sorted_ids = [p.id for p in players_sorted] pairings = generate_round_robin(sorted_ids)[:len(sorted_ids)] for i, (p1, p2) in enumerate(pairings): matches_to_create.append(Match( stage_id=stage_id, round_number=1, match_number=i + 1, player1_id=p1, player2_id=p2, status="scheduled", bracket_position="main", )) for m in matches_to_create: db.add(m) db.commit() return {"ok": True, "match_count": len(matches_to_create)} @router.post("/{event_id}/start") def start_event(event_id: int, db: Session = Depends(get_db)): event = db.query(Event).filter(Event.id == event_id).first() if not event: raise HTTPException(status_code=404, detail="Event not found") event.status = "active" event.started_at = datetime.utcnow() # Activate first stage first_stage = db.query(Stage).filter(Stage.event_id == event_id).order_by(Stage.order).first() if first_stage: first_stage.status = "active" event.current_stage_id = first_stage.id db.commit() return {"ok": True} @router.post("/{event_id}/complete") def complete_event(event_id: int, db: Session = Depends(get_db)): event = db.query(Event).filter(Event.id == event_id).first() if not event: raise HTTPException(status_code=404, detail="Event not found") event.status = "completed" event.completed_at = datetime.utcnow() db.commit() return {"ok": True}