""" Prediction API Routes. This module provides REST endpoints for creating and retrieving match predictions. """ from datetime import datetime from typing import Optional from fastapi import APIRouter, Depends, HTTPException, status, Query from sqlalchemy.orm import Session from app.database import get_db from app.services.prediction_service import PredictionService from app.schemas.prediction import PredictionListResponse, PredictionResponse router = APIRouter(prefix="/api/v1/predictions", tags=["predictions"]) @router.get("", response_model=PredictionListResponse) def get_predictions( limit: int = Query(20, ge=1, le=100, description="Maximum number of predictions to return (max 100)"), offset: int = Query(0, ge=0, description="Number of predictions to skip"), team_id: Optional[int] = Query(None, description="Filter by team ID"), league: Optional[str] = Query(None, description="Filter by league name (case-insensitive)"), date_min: Optional[datetime] = Query(None, description="Filter for matches after this date (ISO 8601)"), date_max: Optional[datetime] = Query(None, description="Filter for matches before this date (ISO 8601)"), db: Session = Depends(get_db) ): """ Get all predictions with pagination and filters. This endpoint retrieves predictions joined with match data, applies optional filters, and returns paginated results sorted by match date (upcoming matches first). Args: limit: Maximum number of predictions to return (1-100, default: 20) offset: Number of predictions to skip (default: 0) team_id: Optional filter by team ID (matches where team is home or away) league: Optional filter by league name (case-insensitive partial match) date_min: Optional filter for matches after this date date_max: Optional filter for matches before this date db: Database session (injected) Returns: Paginated list of predictions with match details and metadata Example Requests: GET /api/v1/predictions GET /api/v1/predictions?limit=10&offset=0 GET /api/v1/predictions?league=Ligue%201 GET /api/v1/predictions?team_id=1&limit=5 GET /api/v1/predictions?date_min=2026-01-15T00:00:00Z&date_max=2026-01-20T23:59:59Z Example Response: { "data": [ { "id": 1, "match_id": 1, "match": { "id": 1, "home_team": "PSG", "away_team": "Olympique de Marseille", "date": "2026-01-18T20:00:00Z", "league": "Ligue 1", "status": "scheduled" }, "energy_score": "high", "confidence": "65.0%", "predicted_winner": "PSG", "created_at": "2026-01-17T12:00:00Z" } ], "meta": { "total": 45, "limit": 20, "offset": 0, "timestamp": "2026-01-17T14:30:00Z", "version": "v1" } } """ service = PredictionService(db) predictions, total = service.get_predictions_with_pagination( limit=limit, offset=offset, team_id=team_id, league=league, date_min=date_min, date_max=date_max ) # Build response with match details prediction_responses = [] for prediction in predictions: match = prediction.match prediction_responses.append({ "id": prediction.id, "match_id": prediction.match_id, "match": { "id": match.id, "home_team": match.home_team, "away_team": match.away_team, "date": match.date.isoformat() if match.date else None, "league": match.league, "status": match.status }, "energy_score": prediction.energy_score, "confidence": prediction.confidence, "predicted_winner": prediction.predicted_winner, "created_at": prediction.created_at.isoformat() if prediction.created_at else None }) return { "data": prediction_responses, "meta": { "total": total, "limit": limit, "offset": offset, "timestamp": datetime.utcnow().isoformat() + "Z", "version": "v1" } } @router.get("/match/{match_id}") def get_prediction_by_match_id( match_id: int, db: Session = Depends(get_db) ): """ Get prediction details for a specific match. This endpoint retrieves the latest prediction for a match and includes full match details, energy score information, and historical predictions. Args: match_id: ID of the match db: Database session (injected) Returns: Prediction with full details including match info and history Raises: 404: If match or prediction not found Example Request: GET /api/v1/predictions/match/1 Example Response: { "id": 3, "match_id": 1, "match": { "id": 1, "home_team": "PSG", "away_team": "Olympique de Marseille", "date": "2026-01-18T20:00:00Z", "league": "Ligue 1", "status": "scheduled", "actual_winner": null }, "energy_score": "high", "confidence": "70.5%", "predicted_winner": "PSG", "created_at": "2026-01-17T14:00:00Z", "history": [ { "id": 3, "energy_score": "high", "confidence": "70.5%", "predicted_winner": "PSG", "created_at": "2026-01-17T14:00:00Z" }, { "id": 2, "energy_score": "medium", "confidence": "60.0%", "predicted_winner": "PSG", "created_at": "2026-01-17T12:00:00Z" } ] } """ service = PredictionService(db) prediction_details = service.get_prediction_with_details(match_id) if not prediction_details: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"No predictions found for match {match_id}" ) return prediction_details @router.post("/matches/{match_id}/predict", response_model=PredictionResponse, status_code=status.HTTP_201_CREATED) def create_prediction( match_id: int, home_energy: float, away_energy: float, energy_score_label: str | None = None, db: Session = Depends(get_db) ): """ Create a prediction for a specific match. This endpoint calculates a prediction based on energy scores for both teams and stores it in the database. Args: match_id: ID of the match to predict home_energy: Energy score of the home team (0.0+) away_energy: Energy score of the away team (0.0+) energy_score_label: Optional label for energy score (e.g., "high", "medium", "low") db: Database session (injected) Returns: Created prediction object Raises: 404: If match doesn't exist 400: If energy scores are invalid 422: If validation fails Example Request: POST /api/v1/predictions/matches/1/predict?home_energy=65.0&away_energy=45.0 Example Response: { "id": 1, "match_id": 1, "energy_score": "high", "confidence": "40.0%", "predicted_winner": "PSG", "created_at": "2026-01-17T12:00:00Z" } """ try: service = PredictionService(db) prediction = service.create_prediction_for_match( match_id=match_id, home_energy=home_energy, away_energy=away_energy, energy_score_label=energy_score_label ) return prediction.to_dict() except ValueError as e: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=str(e) ) except Exception as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to create prediction: {str(e)}" ) @router.get("/{prediction_id}", response_model=PredictionResponse) def get_prediction( prediction_id: int, db: Session = Depends(get_db) ): """ Get a prediction by its ID. Args: prediction_id: ID of the prediction to retrieve db: Database session (injected) Returns: Prediction object Raises: 404: If prediction doesn't exist Example Request: GET /api/v1/predictions/1 Example Response: { "id": 1, "match_id": 1, "energy_score": "high", "confidence": "40.0%", "predicted_winner": "PSG", "created_at": "2026-01-17T12:00:00Z" } """ service = PredictionService(db) prediction = service.get_prediction_by_id(prediction_id) if not prediction: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Prediction with id {prediction_id} not found" ) return prediction.to_dict() @router.get("/matches/{match_id}", response_model=PredictionListResponse) def get_predictions_for_match( match_id: int, db: Session = Depends(get_db) ): """ Get all predictions for a specific match. Args: match_id: ID of the match db: Database session (injected) Returns: List of predictions for the match Example Request: GET /api/v1/predictions/matches/1 Example Response: { "data": [ { "id": 1, "match_id": 1, "energy_score": "high", "confidence": "40.0%", "predicted_winner": "PSG", "created_at": "2026-01-17T12:00:00Z" } ], "count": 1, "meta": {} } """ service = PredictionService(db) predictions = service.get_predictions_for_match(match_id) return { "data": [pred.to_dict() for pred in predictions], "count": len(predictions), "meta": { "match_id": match_id } } @router.get("/matches/{match_id}/latest", response_model=PredictionResponse) def get_latest_prediction_for_match( match_id: int, db: Session = Depends(get_db) ): """ Get the most recent prediction for a specific match. Args: match_id: ID of the match db: Database session (injected) Returns: Latest prediction object Raises: 404: If no predictions exist for the match Example Request: GET /api/v1/predictions/matches/1/latest Example Response: { "id": 3, "match_id": 1, "energy_score": "high", "confidence": "45.0%", "predicted_winner": "PSG", "created_at": "2026-01-17T14:00:00Z" } """ service = PredictionService(db) prediction = service.get_latest_prediction_for_match(match_id) if not prediction: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"No predictions found for match {match_id}" ) return prediction.to_dict() @router.delete("/{prediction_id}", status_code=status.HTTP_204_NO_CONTENT) def delete_prediction( prediction_id: int, db: Session = Depends(get_db) ): """ Delete a prediction by its ID. Args: prediction_id: ID of the prediction to delete db: Database session (injected) Returns: No content (204) Raises: 404: If prediction doesn't exist Example Request: DELETE /api/v1/predictions/1 Example Response: (HTTP 204 No Content) """ service = PredictionService(db) deleted = service.delete_prediction(prediction_id) if not deleted: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Prediction with id {prediction_id} not found" ) return None